diff --git a/.github/actions/cached-node-modules/action.yml b/.github/actions/cached-node-modules/action.yml index bddc11a9dd..a5dbabd829 100644 --- a/.github/actions/cached-node-modules/action.yml +++ b/.github/actions/cached-node-modules/action.yml @@ -34,11 +34,12 @@ runs: - name: Build packages # Regardless of whether the cache was hit or not, we need to build the packages, unless the caller says otherwise if: inputs.build == 'true' - # We build the shared package first, then the others in parallel to speed up the process + # We build the commons and jmspath packages first, then the others in parallel to speed up the process # even though we could just run `npm run build` in the root folder and build them in # sequence, but still in the correct order. run: | - npm run build -w packages/commons + npm run build -w packages/commons + npm run build -w packages/jmespath npm run build -w packages/logger & \ npm run build -w packages/tracer & \ npm run build -w packages/metrics & \ @@ -46,5 +47,5 @@ runs: npm run build -w packages/idempotency & \ npm run build -w packages/batch & \ npm run build -w packages/testing & \ - npm run build -w packages/jmespath + npm run build -w packages/parser shell: bash \ No newline at end of file diff --git a/.github/scripts/release_patch_package_json.js b/.github/scripts/release_patch_package_json.js index 761c0680a1..54292fed35 100644 --- a/.github/scripts/release_patch_package_json.js +++ b/.github/scripts/release_patch_package_json.js @@ -18,7 +18,7 @@ if (process.argv.length < 3) { const basePath = resolve(process.argv[2]); const packageJsonPath = join(basePath, 'package.json'); const alphaPackages = []; -const betaPackages = []; +const betaPackages = ['@aws-lambda-powertools/parser']; (() => { try { @@ -94,4 +94,4 @@ const betaPackages = []; } catch (err) { throw err; } -})(); +})(); \ No newline at end of file diff --git a/.github/workflows/make-version.yml b/.github/workflows/make-version.yml index 7440fc92a6..e61652cd03 100644 --- a/.github/workflows/make-version.yml +++ b/.github/workflows/make-version.yml @@ -21,6 +21,7 @@ jobs: uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: 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@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: diff --git a/.github/workflows/ossf_scorecard.yml b/.github/workflows/ossf_scorecard.yml index 582ab89b9b..fcad19ea73 100644 --- a/.github/workflows/ossf_scorecard.yml +++ b/.github/workflows/ossf_scorecard.yml @@ -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@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10 + uses: github/codeql-action/upload-sarif@df5a14dc28094dc936e103b37d749c6628682b60 # v3.25.0 with: sarif_file: results.sarif 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 fa4e7bd9b2..b4936f91c3 100644 --- a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml +++ b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml @@ -28,9 +28,9 @@ jobs: with: nodeVersion: ${{ matrix.version }} - name: Run linting - run: npm run lint -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency -w packages/batch -w packages/jmespath + run: npm run lint -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency -w packages/batch -w packages/jmespath -w packages/parser - name: Run unit tests - run: npm t -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency -w packages/batch -w packages/jmespath + run: npm t -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency -w packages/batch -w packages/jmespath -w packages/parser check-examples: runs-on: ubuntu-latest env: diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index 88654c6cc9..e99b01260d 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -21,6 +21,7 @@ jobs: id-token: write # needed to interact with GitHub's OIDC Token endpoint. contents: read strategy: + max-parallel: 30 matrix: package: [ diff --git a/.gitignore b/.gitignore index 1dde8041bc..17c364eaab 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,4 @@ tmp # TS build files tsconfig.tsbuildinfo -.tsbuildinfo +.tsbuildinfo \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push index 697956237b..26e9640f00 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,7 +1,9 @@ npm t \ -w packages/commons \ + -w packages/jmespath \ -w packages/logger \ -w packages/metrics \ -w packages/tracer \ -w packages/idempotency \ - -w packages/parameters \ No newline at end of file + -w packages/parameters \ + -w packages/parser \ No newline at end of file diff --git a/.npmignore b/.npmignore index ec8c75c6f9..d502d47a85 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,5 @@ src tests -jest.config.js tsconfig.json .vscode .github @@ -13,7 +12,7 @@ coverage tslint.json tsconfig.json MakeFile -jest.config.js +jest.config.cjs .npmignore .eslintignore .huskyrc.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 569799486a..15e81b9a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.1.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.0.4...v2.1.0) (2024-04-17) + + +### Bug Fixes + +* **jmespath:** refactor custom function introspection to work with minification ([#2384](https://github.com/aws-powertools/powertools-lambda-typescript/issues/2384)) ([21ecc4f](https://github.com/aws-powertools/powertools-lambda-typescript/commit/21ecc4f736ccba85c276889163860a98613174cc)) + + +### Features + +* **idempotency:** add custom JMESPath functions ([#2364](https://github.com/aws-powertools/powertools-lambda-typescript/issues/2364)) ([9721e7c](https://github.com/aws-powertools/powertools-lambda-typescript/commit/9721e7c01fc010944eb477bdbc24b9e06a5c4571)) + + + + + ## 2.0.4 (2024-04-10) ### Bug Fixes diff --git a/docs/Dockerfile b/docs/Dockerfile index 87302bb7c0..01b7a9cc8f 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -1,5 +1,5 @@ # version 9.5.2 -FROM squidfunk/mkdocs-material@sha256:6b124e13728a591607e0f087920763e405fc18861736a997896d0b253867a7b7 +FROM squidfunk/mkdocs-material@sha256:521644b58bc0c806083ef66e8b1027861bd3e98c433b251c436c5c0cc7733603 ADD requirements.txt /tmp/ RUN pip install --require-hashes -r /tmp/requirements.txt \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index a14d067a31..a4ecfa7db8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -61,14 +61,13 @@ You can use Powertools for AWS Lambda (TypeScript) by installing it with your fa | **[Parameters (AppConfig)](./utilities/parameters.md#install)** | **`npm i @aws-lambda-powertools/parameters @aws-sdk/client-appconfigdata`**{.copyMe}:clipboard: | | | **[Parser](./utilities/parser.md#install)** | **`npm i @aws-lambda-powertools/parser zod@~3`**{.copyMe}:clipboard: | | - === "Lambda Layer" You can add our layer both in the [AWS Lambda Console _(under `Layers`)_](https://eu-west-1.console.aws.amazon.com/lambda/home#/add/layer){target="_blank"}, or via your favorite infrastructure as code framework with the ARN value. For the latter, make sure to replace `{region}` with your AWS region, e.g., `eu-west-1`. - __arn:aws:lambda:{region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3__{: .copyMe}:clipboard: + __arn:aws:lambda:{region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4__{: .copyMe}:clipboard: ???+ note "Code snippets for popular infrastructure as code frameworks" @@ -125,7 +124,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:3 + - !Sub arn:aws:lambda:${AWS::Region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4 ``` If you use `esbuild` to bundle your code, make sure to exclude `@aws-lambda-powertools` from being bundled since the packages will be already present the Layer: @@ -156,7 +155,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:3 + - arn:aws:lambda:${aws:region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4 ``` If you use `esbuild` to bundle your code, make sure to exclude `@aws-lambda-powertools` from being bundled since the packages will be already present the Layer: @@ -193,7 +192,7 @@ You can use Powertools for AWS Lambda (TypeScript) by installing it with your fa role = ... handler = "index.handler" runtime = "nodejs16.x" - layers = ["arn:aws:lambda:{aws::region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3"] + layers = ["arn:aws:lambda:{aws::region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4"] source_code_hash = filebase64sha256("lambda_function_payload.zip") } ``` @@ -211,7 +210,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:3` + pulumi.interpolate`arn:aws:lambda:${aws.getRegionOutput().name}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4` ], code: new pulumi.asset.FileArchive('lambda_function_payload.zip'), tracingConfig: { @@ -235,7 +234,7 @@ You can use Powertools for AWS Lambda (TypeScript) by installing it with your fa ? Do you want to configure advanced settings? Yes ... ? Do you want to enable Lambda layers for this function? Yes - ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:{aws::region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3 + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:{aws::region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4 ❯ amplify push -y # Updating an existing function and add the layer @@ -245,7 +244,7 @@ You can use Powertools for AWS Lambda (TypeScript) by installing it with your fa - Name: ? Which setting do you want to update? Lambda layers configuration ? Do you want to enable Lambda layers for this function? Yes - ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:{aws::region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3 + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:{aws::region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4 ? Do you want to edit the local lambda function now? No ``` @@ -255,36 +254,38 @@ You can use Powertools for AWS Lambda (TypeScript) by installing it with your fa [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. +You can use the Lambda Layer both with CommonJS and ESM (ECMAScript modules) for Node.js 18.x and newer runtimes. **If you are using the managed Node.js 16.x runtime and cannot upgrade, you should use the CommonJS version only**. + ??? 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:3](#){: .copyMe}:clipboard: | - | `us-east-2` | [arn:aws:lambda:us-east-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `us-west-1` | [arn:aws:lambda:us-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `us-west-2` | [arn:aws:lambda:us-west-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `ap-south-1` | [arn:aws:lambda:ap-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `ap-east-1` | [arn:aws:lambda:ap-east-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `ap-southeast-4` | [arn:aws:lambda:ap-southeast-4:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `eu-central-1` | [arn:aws:lambda:eu-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `eu-central-2` | [arn:aws:lambda:eu-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `eu-west-1` | [arn:aws:lambda:eu-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `eu-west-2` | [arn:aws:lambda:eu-west-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `eu-west-3` | [arn:aws:lambda:eu-west-3:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `eu-north-1` | [arn:aws:lambda:eu-north-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `eu-south-1` | [arn:aws:lambda:eu-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `eu-south-2` | [arn:aws:lambda:eu-south-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `ca-central-1` | [arn:aws:lambda:ca-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `ca-west-1` | [arn:aws:lambda:ca-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `sa-east-1` | [arn:aws:lambda:sa-east-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `af-south-1` | [arn:aws:lambda:af-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `me-south-1` | [arn:aws:lambda:me-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | - | `il-central-1` | [arn:aws:lambda:il-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | + | `us-east-1` | [arn:aws:lambda:us-east-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `us-east-2` | [arn:aws:lambda:us-east-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `us-west-1` | [arn:aws:lambda:us-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `us-west-2` | [arn:aws:lambda:us-west-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `ap-south-1` | [arn:aws:lambda:ap-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `ap-east-1` | [arn:aws:lambda:ap-east-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `ap-southeast-3` | [arn:aws:lambda:ap-southeast-3:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `ap-southeast-4` | [arn:aws:lambda:ap-southeast-4:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `eu-central-1` | [arn:aws:lambda:eu-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `eu-central-2` | [arn:aws:lambda:eu-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `eu-west-1` | [arn:aws:lambda:eu-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `eu-west-2` | [arn:aws:lambda:eu-west-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `eu-west-3` | [arn:aws:lambda:eu-west-3:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `eu-north-1` | [arn:aws:lambda:eu-north-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `eu-south-1` | [arn:aws:lambda:eu-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `eu-south-2` | [arn:aws:lambda:eu-south-2:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `ca-central-1` | [arn:aws:lambda:ca-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `ca-west-1` | [arn:aws:lambda:ca-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `sa-east-1` | [arn:aws:lambda:sa-east-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `af-south-1` | [arn:aws:lambda:af-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `me-south-1` | [arn:aws:lambda:me-south-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | + | `il-central-1` | [arn:aws:lambda:il-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4](#){: .copyMe}:clipboard: | **Want to inspect the contents of the Layer?** @@ -294,7 +295,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:3 --region {aws::region} +aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{aws::region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:4 --region {aws::region} ``` ## Instrumentation @@ -325,11 +326,12 @@ Core utilities such as Tracing, Logging, and Metrics will be available across al | [Parameters](./utilities/parameters.md) | High-level functions to retrieve one or more parameters from AWS SSM Parameter Store, AWS Secrets Manager, AWS AppConfig, and Amazon DynamoDB | | [Idempotency](./utilities/idempotency.md) | Class method decorator, Middy middleware, and function wrapper to make your Lambda functions idempotent and prevent duplicate execution based on payload content. | | [Batch Processing](./utilities/batch.md) | Utility to handle partial failures when processing batches from Amazon SQS, Amazon Kinesis Data Streams, and Amazon DynamoDB Streams. | +| [Parser](./utilities/parser.md) | Utility to parse and validate AWS Lambda event payloads using Zod, a TypeScript-first schema declaration and validation library. | ## Environment variables ???+ info - Explicit parameters take precedence over environment variables + Explicit parameters take precedence over environment variables | Environment variable | Description | Utility | Default | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ------------------- | @@ -438,4 +440,4 @@ These are our core principles to guide our decision making. - __Keep it lean__. Additional dependencies are carefully considered for security and ease of maintenance, and prevent negatively impacting startup time. - __We strive for backwards compatibility__. New features and changes should keep backwards compatibility. If a breaking change cannot be avoided, the deprecation and migration process should be clearly defined. - __We work backwards from the community__. We aim to strike a balance of what would work best for 80% of customers. Emerging practices are considered and discussed via Requests for Comment (RFCs) -- __Progressive__. Utilities are designed to be incrementally adoptable for customers at any stage of their Serverless journey. They follow language idioms and their community’s common practices. \ No newline at end of file +- __Progressive__. Utilities are designed to be incrementally adoptable for customers at any stage of their Serverless journey. They follow language idioms and their community’s common practices. diff --git a/docs/requirements.in b/docs/requirements.in index ab56f35022..169f7d2969 100644 --- a/docs/requirements.in +++ b/docs/requirements.in @@ -1,4 +1,4 @@ mike==1.1.2 -mkdocs-material==9.5.17 +mkdocs-material==9.5.18 mkdocs-git-revision-date-plugin==0.3.2 mkdocs-exclude==1.0.2 \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 69937287c3..2ec21c5375 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -124,9 +124,9 @@ gitpython==3.1.41 \ --hash=sha256:c36b6634d069b3f719610175020a9aed919421c87552185b085e04fbbdb10b7c \ --hash=sha256:ed66e624884f76df22c8e16066d567aaa5a37d5b5fa19db2c6df6f7156db9048 # via mkdocs-git-revision-date-plugin -idna==3.6 \ - --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ - --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f +idna==3.7 \ + --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ + --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 # via requests jinja2==3.1.3 \ --hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \ @@ -229,9 +229,9 @@ mkdocs-exclude==1.0.2 \ mkdocs-git-revision-date-plugin==0.3.2 \ --hash=sha256:2e67956cb01823dd2418e2833f3623dee8604cdf223bddd005fe36226a56f6ef # via -r requirements.in -mkdocs-material==9.5.17 \ - --hash=sha256:06ae1275a72db1989cf6209de9e9ecdfbcfdbc24c58353877b2bb927dbe413e4 \ - --hash=sha256:14a2a60119a785e70e765dd033e6211367aca9fc70230e577c1cf6a326949571 +mkdocs-material==9.5.18 \ + --hash=sha256:1e0e27fc9fe239f9064318acf548771a4629d5fd5dfd45444fd80a953fe21eb4 \ + --hash=sha256:a43f470947053fa2405c33995f282d24992c752a50114f23f30da9d8d0c57e62 # via -r requirements.in mkdocs-material-extensions==1.3.1 \ --hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \ diff --git a/docs/snippets/CHANGELOG.md b/docs/snippets/CHANGELOG.md index f0e00ab7ca..38c12e9452 100644 --- a/docs/snippets/CHANGELOG.md +++ b/docs/snippets/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.1.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.0.4...v2.1.0) (2024-04-17) + + +### Features + +* **idempotency:** add custom JMESPath functions ([#2364](https://github.com/aws-powertools/powertools-lambda-typescript/issues/2364)) ([9721e7c](https://github.com/aws-powertools/powertools-lambda-typescript/commit/9721e7c01fc010944eb477bdbc24b9e06a5c4571)) + + + + + ## 2.0.4 (2024-04-10) **Note:** Version bump only for package docs diff --git a/docs/snippets/idempotency/makeIdempotentJmes.ts b/docs/snippets/idempotency/makeIdempotentJmes.ts index b4d0d165d7..ddad91ad45 100644 --- a/docs/snippets/idempotency/makeIdempotentJmes.ts +++ b/docs/snippets/idempotency/makeIdempotentJmes.ts @@ -22,9 +22,9 @@ const createSubscriptionPayment = async ( }; }; -// Extract the idempotency key from the request headers +// Deserialize JSON string under the "body" key, then extract the "user" and "productId" keys const config = new IdempotencyConfig({ - eventKeyJmesPath: 'body', + eventKeyJmesPath: 'powertools_json(body).["user", "productId"]', }); export const handler = makeIdempotent( diff --git a/docs/snippets/package.json b/docs/snippets/package.json index e6fd922d8a..8fcd06e5ae 100644 --- a/docs/snippets/package.json +++ b/docs/snippets/package.json @@ -1,6 +1,6 @@ { "name": "docs", - "version": "2.0.4", + "version": "2.1.0", "description": "A collection code snippets for the Powertools for AWS Lambda (TypeScript) docs", "author": { "name": "Amazon Web Services", @@ -29,15 +29,17 @@ }, "homepage": "https://github.com/aws-powertools/powertools-lambda-typescript#readme", "devDependencies": { - "@aws-sdk/client-appconfigdata": "^3.549.0", - "@aws-sdk/client-dynamodb": "^3.549.0", - "@aws-sdk/client-secrets-manager": "^3.549.0", - "@aws-sdk/client-ssm": "^3.549.0", - "@aws-sdk/util-dynamodb": "^3.549.0", - "aws-sdk": "^2.1595.0", + "@aws-sdk/client-appconfigdata": "^3.554.0", + "@aws-sdk/client-dynamodb": "^3.554.0", + "@aws-sdk/client-secrets-manager": "^3.554.0", + "@aws-sdk/client-ssm": "^3.554.0", + "@aws-sdk/util-dynamodb": "^3.554.0", + "@middy/core": "^4.7.0", + "aws-sdk": "^2.1599.0", "aws-sdk-client-mock": "^4.0.0", "aws-sdk-client-mock-jest": "^4.0.0", "axios": "^1.6.8", - "hashi-vault-js": "^0.4.14" + "hashi-vault-js": "^0.4.14", + "zod": "^3.22.4" } } diff --git a/docs/snippets/parser/decorator.ts b/docs/snippets/parser/decorator.ts new file mode 100644 index 0000000000..c1a91ad096 --- /dev/null +++ b/docs/snippets/parser/decorator.ts @@ -0,0 +1,35 @@ +import type { Context } from 'aws-lambda'; +import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { parser } from '@aws-lambda-powertools/parser'; +import { z } from 'zod'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +type Order = z.infer; + +class Lambda implements LambdaInterface { + @parser({ schema: orderSchema }) + public async handler(event: Order, _context: Context): Promise { + // event is now typed as Order + for (const item of event.items) { + logger.info('Processing item', { item }); + } + } +} + +const myFunction = new Lambda(); +export const handler = myFunction.handler.bind(myFunction); diff --git a/docs/snippets/parser/envelopeDecorator.ts b/docs/snippets/parser/envelopeDecorator.ts new file mode 100644 index 0000000000..e54e0f7387 --- /dev/null +++ b/docs/snippets/parser/envelopeDecorator.ts @@ -0,0 +1,36 @@ +import type { Context } from 'aws-lambda'; +import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { parser } from '@aws-lambda-powertools/parser'; +import { z } from 'zod'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +type Order = z.infer; + +class Lambda implements LambdaInterface { + @parser({ schema: orderSchema, envelope: EventBridgeEnvelope }) // (1)! + public async handler(event: Order, _context: Context): Promise { + // event is now typed as Order + for (const item of event.items) { + logger.info('Processing item', item); // (2)! + } + } +} + +const myFunction = new Lambda(); +export const handler = myFunction.handler.bind(myFunction); diff --git a/docs/snippets/parser/envelopeMiddy.ts b/docs/snippets/parser/envelopeMiddy.ts new file mode 100644 index 0000000000..94f8f012af --- /dev/null +++ b/docs/snippets/parser/envelopeMiddy.ts @@ -0,0 +1,37 @@ +import type { Context } from 'aws-lambda'; +import { parser } from '@aws-lambda-powertools/parser/middleware'; +import { z } from 'zod'; +import middy from '@middy/core'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + 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 }) +); diff --git a/docs/snippets/parser/examplePayload.json b/docs/snippets/parser/examplePayload.json new file mode 100644 index 0000000000..225be75a69 --- /dev/null +++ b/docs/snippets/parser/examplePayload.json @@ -0,0 +1,21 @@ +{ + "version": "0", + "id": "6a7e8feb-b491-4cf7-a9f1-bf3703467718", + "detail-type": "OrderPurchased", + "source": "OrderService", + "account": "111122223333", + "time": "2020-10-22T18:43:48Z", + "region": "us-west-1", + "resources": ["some_additional"], + "detail": { + "id": 10876546789, + "description": "My order", + "items": [ + { + "id": 1015938732, + "quantity": 1, + "description": "item xpto" + } + ] + } +} diff --git a/docs/snippets/parser/extend.ts b/docs/snippets/parser/extend.ts new file mode 100644 index 0000000000..d14845c10f --- /dev/null +++ b/docs/snippets/parser/extend.ts @@ -0,0 +1,40 @@ +import type { Context } from 'aws-lambda'; +import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { parser } from '@aws-lambda-powertools/parser'; +import { z } from 'zod'; +import { EventBridgeSchema } from '@aws-lambda-powertools/parser/schemas'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +const orderEventSchema = EventBridgeSchema.extend({ + detail: orderSchema, // (1)! +}); + +type OrderEvent = z.infer; + +class Lambda implements LambdaInterface { + @parser({ schema: orderEventSchema }) // (2)! + public async handler(event: OrderEvent, _context: Context): Promise { + for (const item of event.detail.items) { + // process OrderItem + logger.info('Processing item', { item }); // (3)! + } + } +} + +const myFunction = new Lambda(); +export const handler = myFunction.handler.bind(myFunction); diff --git a/docs/snippets/parser/manual.ts b/docs/snippets/parser/manual.ts new file mode 100644 index 0000000000..2be8204cad --- /dev/null +++ b/docs/snippets/parser/manual.ts @@ -0,0 +1,33 @@ +import type { Context } from 'aws-lambda'; +import { z } from 'zod'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; +import { EventBridgeSchema } from '@aws-lambda-powertools/parser/schemas'; +import type { EventBridgeEvent } from '@aws-lambda-powertools/parser/types'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); +type Order = z.infer; + +export const handler = async ( + event: EventBridgeEvent, + _context: Context +): Promise => { + const parsedEvent = EventBridgeSchema.parse(event); // (1)! + logger.info('Parsed event', parsedEvent); + + const orders: Order = EventBridgeEnvelope.parse(event, orderSchema); // (2)! + logger.info('Parsed orders', orders); +}; diff --git a/docs/snippets/parser/manualSafeParse.ts b/docs/snippets/parser/manualSafeParse.ts new file mode 100644 index 0000000000..17106c1bda --- /dev/null +++ b/docs/snippets/parser/manualSafeParse.ts @@ -0,0 +1,35 @@ +import type { Context } from 'aws-lambda'; +import { z } from 'zod'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; +import { EventBridgeSchema } from '@aws-lambda-powertools/parser/schemas'; +import type { EventBridgeEvent } from '@aws-lambda-powertools/parser/types'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +export const handler = async ( + event: EventBridgeEvent, + _context: Context +): Promise => { + const parsedEvent = EventBridgeSchema.safeParse(event); // (1)! + parsedEvent.success + ? logger.info('Event parsed successfully', parsedEvent.data) + : logger.error('Event parsing failed', parsedEvent.error); + const parsedEvenlope = EventBridgeEnvelope.safeParse(event, orderSchema); // (2)! + parsedEvenlope.success + ? logger.info('Event envelope parsed successfully', parsedEvenlope.data) + : logger.error('Event envelope parsing failed', parsedEvenlope.error); +}; diff --git a/docs/snippets/parser/middy.ts b/docs/snippets/parser/middy.ts new file mode 100644 index 0000000000..ff3bf3a879 --- /dev/null +++ b/docs/snippets/parser/middy.ts @@ -0,0 +1,36 @@ +import type { Context } from 'aws-lambda'; +import { parser } from '@aws-lambda-powertools/parser/middleware'; +import { z } from 'zod'; +import middy from '@middy/core'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + 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 }) +); diff --git a/docs/snippets/parser/refine.ts b/docs/snippets/parser/refine.ts new file mode 100644 index 0000000000..b3e58fad08 --- /dev/null +++ b/docs/snippets/parser/refine.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +const orderItemSchema = z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), +}); + +export const orderSchema = z + .object({ + id: z.number().positive(), + description: z.string(), + items: z.array(orderItemSchema).refine((items) => items.length > 0, { + message: 'Order must have at least one item', // (1)! + }), + optionalField: z.string().optional(), + }) + .refine((order) => order.id > 100 && order.items.length > 100, { + message: + 'All orders with more than 100 items must have an id greater than 100', // (2)! + }); diff --git a/docs/snippets/parser/safeParseDecorator.ts b/docs/snippets/parser/safeParseDecorator.ts new file mode 100644 index 0000000000..aaf4d19ca9 --- /dev/null +++ b/docs/snippets/parser/safeParseDecorator.ts @@ -0,0 +1,47 @@ +import type { Context } from 'aws-lambda'; +import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { parser } from '@aws-lambda-powertools/parser'; +import { z } from 'zod'; +import type { + ParsedResult, + EventBridgeEvent, +} from '@aws-lambda-powertools/parser/types'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +type Order = z.infer; + +class Lambda implements LambdaInterface { + @parser({ schema: orderSchema, 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)! + } + } else { + logger.error('Failed to parse event', event.error); // (4)! + logger.error('Original event is: ', event.originalEvent); // (5)! + } + } +} + +const myFunction = new Lambda(); +export const handler = myFunction.handler.bind(myFunction); diff --git a/docs/snippets/parser/safeParseMiddy.ts b/docs/snippets/parser/safeParseMiddy.ts new file mode 100644 index 0000000000..547eae73b7 --- /dev/null +++ b/docs/snippets/parser/safeParseMiddy.ts @@ -0,0 +1,45 @@ +import type { Context } from 'aws-lambda'; +import { parser } from '@aws-lambda-powertools/parser/middleware'; +import { z } from 'zod'; +import middy from '@middy/core'; +import type { + ParsedResult, + EventBridgeEvent, +} from '@aws-lambda-powertools/parser/types'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + 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)! + } + } 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/docs/snippets/parser/schema.ts b/docs/snippets/parser/schema.ts new file mode 100644 index 0000000000..f14db32fff --- /dev/null +++ b/docs/snippets/parser/schema.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +export { orderSchema }; diff --git a/docs/snippets/parser/types.ts b/docs/snippets/parser/types.ts new file mode 100644 index 0000000000..eba3862835 --- /dev/null +++ b/docs/snippets/parser/types.ts @@ -0,0 +1,36 @@ +import type { Context } from 'aws-lambda'; +import { parser } from '@aws-lambda-powertools/parser/middleware'; +import { z } from 'zod'; +import middy from '@middy/core'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +type Order = z.infer; // (1)! + +const lambdaHandler = async ( + event: Order, // (2)! + _context: Context +): Promise => { + for (const item of event.items) { + // item is parsed as OrderItem + logger.info('Processing item', { item }); // (3)! + } +}; + +export const handler = middy(lambdaHandler).use( + parser({ schema: orderSchema }) +); diff --git a/docs/snippets/tsconfig.json b/docs/snippets/tsconfig.json index d6aec30ce7..7c921276c8 100644 --- a/docs/snippets/tsconfig.json +++ b/docs/snippets/tsconfig.json @@ -4,6 +4,8 @@ "rootDir": "./", "baseUrl": ".", "noEmit": true, + "noUnusedLocals": false, + "allowUnusedLabels": true, "paths": { "@aws-lambda-powertools/parameters/ssm": [ "../../packages/parameters/lib/ssm" @@ -31,7 +33,9 @@ "@aws-lambda-powertools/jmespath": ["../../packages/jmespath/lib"], "@aws-lambda-powertools/jmespath/envelopes": [ "../../packages/jmespath/lib/envelopes" - ] + ], + "@aws-lambda-powertools/parser": ["../../packages/parser/lib"], + "@aws-lambda-powertools/logger": ["../../packages/logger/lib"] } } } diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index 60dcd7aae3..273d7a551b 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -220,9 +220,11 @@ Imagine the function executes successfully, but the client never receives the re ???+ warning "Deserializing JSON strings in payloads for increased accuracy." The payload extracted by the `eventKeyJmesPath` is treated as a string by default. This means there could be differences in whitespace even when the JSON payload itself is identical. + To alter this behaviour, we can use the [JMESPath built-in function `powertools_json()`](jmespath.md#powertools_json-function) to treat the payload as a JSON object rather than a string. + === "index.ts" - ```typescript hl_lines="4 26-28 49" + ```typescript hl_lines="4 27 49" --8<-- "docs/snippets/idempotency/makeIdempotentJmes.ts" ``` diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md new file mode 100644 index 0000000000..4947f86a2c --- /dev/null +++ b/docs/utilities/parser.md @@ -0,0 +1,255 @@ +--- +title: Parser (Zod) +descrition: Utility +status: new +--- + +This utility provides data validation and parsing using [Zod](https://zod.dev){target="_blank"}. + +Zod is a TypeScript-first schema declaration and validation library. + +## Key features + +* Define data schema as Zod schema, then parse, validate and extract only what you want +* Built-in envelopes to unwrap and validate popular AWS event sources payloads +* Extend and customize envelopes to fit your needs +* Safe parsing option to avoid throwing errors and custom error handling +* Available for Middy.js middleware and TypeScript method decorators + +## Getting started + +### Install + +```bash +npm install @aws-lambda-powertools/parser zod@~3 +``` + +This utility supports Zod v3.x and above. + +## Define schema + +You can define your schema using Zod: + +```typescript title="schema.ts" +--8<-- "docs/snippets/parser/schema.ts" +``` + +This is a schema for `Order` object using Zod. +You can create complex schemas by using nested objects, arrays, unions, and other types, see [Zod documentation](https://zod.dev) for more details. + +## Parse events + +You can parse inbound events using `parser` decorator, Middy.js middleware, or [manually](#manual-parsing) using built-in envelopes and schemas. +Both are also able to parse either an object or JSON string as an input. + +???+ warning + The decorator and middleware will replace the event object with the parsed schema if successful. + 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" + --8<-- "docs/snippets/parser/middy.ts" + ``` + +=== "Decorator" + ```typescript hl_lines="25" + --8<-- "docs/snippets/parser/decorator.ts" + ``` + +## Built-in schemas + + +Parser comes with the following built-in schemas: + +| Model name | Description | +| -------------------------------------------- | ------------------------------------------------------------------------------------- | +| **AlbSchema** | Lambda Event Source payload for Amazon Application Load Balancer | +| **APIGatewayProxyEventSchema** | Lambda Event Source payload for Amazon API Gateway | +| **APIGatewayProxyEventV2Schema** | Lambda Event Source payload for Amazon API Gateway v2 payload | +| **CloudFormationCustomResourceCreateSchema** | Lambda Event Source payload for AWS CloudFormation `CREATE` operation | +| **CloudFormationCustomResourceUpdateSchema** | Lambda Event Source payload for AWS CloudFormation `UPDATE` operation | +| **CloudFormationCustomResourceDeleteSchema** | Lambda Event Source payload for AWS CloudFormation `DELETE` operation | +| **CloudwatchLogsSchema** | Lambda Event Source payload for Amazon CloudWatch Logs | +| **DynamoDBStreamSchema** | Lambda Event Source payload for Amazon DynamoDB Streams | +| **EventBridgeSchema** | Lambda Event Source payload for Amazon EventBridge | +| **KafkaMskEventSchema** | Lambda Event Source payload for AWS MSK payload | +| **KafkaSelfManagedEventSchema** | Lambda Event Source payload for self managed Kafka payload | +| **KinesisDataStreamSchema** | Lambda Event Source payload for Amazon Kinesis Data Streams | +| **KinesisFirehoseSchema** | Lambda Event Source payload for Amazon Kinesis Firehose | +| **KinesisFirehoseSqsSchema** | Lambda Event Source payload for SQS messages wrapped in Kinesis Firehose records | +| **LambdaFunctionUrlSchema** | Lambda Event Source payload for Lambda Function URL payload | +| **S3EventNotificationEventBridgeSchema** | Lambda Event Source payload for Amazon S3 Event Notification to EventBridge. | +| **S3Schema** | Lambda Event Source payload for Amazon S3 | +| **S3ObjectLambdaEvent** | Lambda Event Source payload for Amazon S3 Object Lambda | +| **S3SqsEventNotificationSchema** | Lambda Event Source payload for S3 event notifications wrapped in SQS event (S3->SQS) | +| **SesSchema** | Lambda Event Source payload for Amazon Simple Email Service | +| **SnsSchema** | Lambda Event Source payload for Amazon Simple Notification Service | +| **SqsSchema** | Lambda Event Source payload for Amazon SQS | +| **VpcLatticeSchema** | Lambda Event Source payload for Amazon VPC Lattice | +| **VpcLatticeV2Schema** | Lambda Event Source payload for Amazon VPC Lattice v2 payload | + +### Extend built-in schemas + +You can extend every built-in schema to include your own schema, and yet have all other known fields parsed along the way. + +=== "handler.ts" + ```typescript hl_lines="23-25 30 34" + --8<-- "docs/snippets/parser/extend.ts" + ``` + + 1. Extend built-in `EventBridgeSchema` with your own detail schema + 2. Pass the extended schema to `parser` decorator or middy middleware + 3. `event` is validated including your custom schema and now available in your handler + + +=== "Example payload" + + ```json + --8<-- "docs/snippets/parser/examplePayload.json" + ``` + +## Envelopes + +When trying to parse your payload you might encounter the following situations: + +* Your actual payload is wrapped around a known structure, for example Lambda Event Sources like EventBridge +* You're only interested in a portion of the payload, for example parsing the detail of custom events in EventBridge, or body of SQS records +* You can either solve these situations by creating a schema of these known structures, parsing them, then extracting and parsing a key where your payload is. + +This can become difficult quite quickly. Parser simplifies the development through a feature named Envelope. +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" + --8<-- "docs/snippets/parser/envelopeMiddy.ts" + ``` + +=== "Decorator" + ```typescript hl_lines="5 26 30" + --8<-- "docs/snippets/parser/envelopeDecorator.ts" + ``` + + 1. Pass `eventBridgeEnvelope` to `parser` decorator + 2. `event` is parsed and replaced as `Order` object + + + +The envelopes are functions that take an event and the schema to parse, and return the result of the inner schema. +Depending on the envelope it can be something simple like extracting a key. +We have also complex envelopes that parse the payload from a string, decode base64, uncompress gzip, etc. + +!!! tip "Envelopes vs schema extension" + Use envelopes if you want to extract only the inner part of an event payload and don't use the information from the Lambda event. + Otherwise, extend built-in schema to parse the whole payload and use the metadata from the Lambda event. + +### Built-in envelopes + +Parser comes with the following built-in envelopes: + +| Envelope name | Behaviour | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **apiGatewayEnvelope** | 1. Parses data using `APIGatewayProxyEventSchema`.
2. Parses `body` key using your schema and returns it. | +| **apiGatewayV2Envelope** | 1. Parses data using `APIGatewayProxyEventV2Schema`.
2. Parses `body` key using your schema and returns it. | +| **cloudWatchEnvelope** | 1. Parses data using `CloudwatchLogsSchema` which will base64 decode and decompress it.
2. Parses records in `message` key using your schema and return them in a list. | +| **dynamoDBStreamEnvelope** | 1. Parses data using `DynamoDBStreamSchema`.
2. Parses records in `NewImage` and `OldImage` keys using your schema.
3. Returns a list with a dictionary containing `NewImage` and `OldImage` keys | +| **eventBridgeEnvelope** | 1. Parses data using `EventBridgeSchema`.
2. Parses `detail` key using your schema and returns it. | +| **kafkaEnvelope** | 1. Parses data using `KafkaRecordSchema`.
2. Parses `value` key using your schema and returns it. | +| **kinesisEnvelope** | 1. Parses data using `KinesisDataStreamSchema` which will base64 decode it.
2. Parses records in `Records` key using your schema and returns them in a list. | +| **kinesisFirehoseEnvelope** | 1. Parses data using `KinesisFirehoseSchema` which will base64 decode it.
2. Parses records in `Records` key using your schema and returns them in a list. | +| **lambdaFunctionUrlEnvelope** | 1. Parses data using `LambdaFunctionUrlSchema`.
2. Parses `body` key using your schema and returns it. | +| **snsEnvelope** | 1. Parses data using `SnsSchema`.
2. Parses records in `body` key using your schema and return them in a list. | +| **snsSqsEnvelope** | 1. Parses data using `SqsSchema`.
2. Parses SNS records in `body` key using `SnsNotificationSchema`.
3. Parses data in `Message` key using your schema and return them in a list. | +| **sqsEnvelope** | 1. Parses data using `SqsSchema`.
2. Parses records in `body` key using your schema and return them in a list. | +| **vpcLatticeEnvelope** | 1. Parses data using `VpcLatticeSchema`.
2. Parses `value` key using your schema and returns it. | +| **vpcLatticeV2Envelope** | 1. Parses data using `VpcLatticeSchema`.
2. Parses `value` key using your schema and returns it. | + + +## Safe parsing + +If you want to parse the event without throwing an error, use the `safeParse` option. +The handler `event` object will be replaced with `ParsedResult`, for example `ParsedResult`, where `SqsEvent` is the original event and `Order` is the parsed schema. + +The `ParsedResult` object will have `success`, `data`, or `error` and `originalEvent` fields, depending on the outcome. +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" + --8<-- "docs/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 + +=== "Decorator" + ```typescript hl_lines="29 35 37 40 41" + --8<-- "docs/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 + + +## Manual parsing + +You can use built-in envelopes and schemas to parse the incoming events manually, without using middy or decorator. + + +=== "Manual parse" + ```typescript hl_lines="28 31" + --8<-- "docs/snippets/parser/manual.ts" + ``` + + 1. Use `EventBridgeSchema` to parse the event, the `details` fields will be parsed as a generic record. + 2. Use `eventBridgeEnvelope` with a combination of `orderSchema` to get `Order` object from the `details` field. + +=== "Manual safeParse" + ```typescript hl_lines="27 31" + --8<-- "docs/snippets/parser/manualSafeParse.ts" + ``` + + 1. Use `safeParse` option to parse the event without throwing an error + 2. `safeParse` is also available for envelopes + +## Custom validation + +Because Parser uses Zod, you can use all the features of Zod to validate your data. +For example, you can use `refine` to validate a field or a combination of fields: + +=== "Custom validation" + ```typescript hl_lines="13 18" + --8<-- "docs/snippets/parser/refine.ts" + ``` + + 1. validate a single field + 2. validate an object with multiple fields + +Zod provides a lot of other features and customization, see [Zod documentation](https://zod.dev) for more details. + + +## Types + +### Schema and Type inference +Use `z.infer` to extract the type of the schema, so you can use types during development and avoid type errors. + +=== "Types" + ```typescript hl_lines="22 25 30" + --8<-- "docs/snippets/parser/types.ts" + ``` + + 1. Use `z.infer` to extract the type of the schema, also works for nested schemas + 2. `event` is of type `Order` + 3. infer types from deeply nested schemas + +### Compatibility with @types/aws-lambda + +The package `@types/aws-lambda` is a popular project that contains type definitions for many AWS service event invocations. +Powertools parser utility also bring AWS Lambda event types based on the built-in schema definitions. + +We recommend to use the types provided by the parser utility. If you encounter any issues or have any feedback, please [submit an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose). diff --git a/examples/app/CHANGELOG.md b/examples/app/CHANGELOG.md index 9b1ba0882f..89123e041c 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.1.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.0.4...v2.1.0) (2024-04-17) + +**Note:** Version bump only for package powertools-sample-app + + + + + ## 2.0.4 (2024-04-10) **Note:** Version bump only for package powertools-sample-app diff --git a/examples/app/package.json b/examples/app/package.json index ca80b309a7..2a42f380a0 100644 --- a/examples/app/package.json +++ b/examples/app/package.json @@ -1,6 +1,6 @@ { "name": "powertools-sample-app", - "version": "2.0.4", + "version": "2.1.0", "author": { "name": "Amazon Web Services", "url": "https://aws.amazon.com" @@ -34,35 +34,35 @@ "devDependencies": { "@types/aws-lambda": "^8.10.137", "@types/jest": "^29.5.12", - "@types/node": "20.12.6", - "aws-cdk": "^2.136.0", - "aws-cdk-lib": "^2.136.0", + "@types/node": "20.12.7", + "aws-cdk": "^2.137.0", + "aws-cdk-lib": "^2.137.0", "constructs": "^10.3.0", "jest": "^29.7.0", "source-map-support": "^0.5.21", "ts-jest": "^29.1.2", "tsx": "^4.7.2", - "typescript": "^5.4.4" + "typescript": "^5.4.5" }, "dependencies": { - "@aws-lambda-powertools/batch": "^2.0.4", - "@aws-lambda-powertools/idempotency": "^2.0.4", - "@aws-lambda-powertools/logger": "^2.0.4", - "@aws-lambda-powertools/metrics": "^2.0.4", - "@aws-lambda-powertools/parameters": "^2.0.4", - "@aws-lambda-powertools/tracer": "^2.0.4", - "@aws-sdk/client-ssm": "^3.549.0", - "@aws-sdk/lib-dynamodb": "^3.549.0", + "@aws-lambda-powertools/batch": "^2.1.0", + "@aws-lambda-powertools/idempotency": "^2.1.0", + "@aws-lambda-powertools/logger": "^2.1.0", + "@aws-lambda-powertools/metrics": "^2.1.0", + "@aws-lambda-powertools/parameters": "^2.1.0", + "@aws-lambda-powertools/tracer": "^2.1.0", + "@aws-sdk/client-ssm": "^3.554.0", + "@aws-sdk/lib-dynamodb": "^3.554.0", "@middy/core": "^4.7.0", "@types/aws-lambda": "^8.10.137", "@types/jest": "^29.5.12", - "@types/node": "20.12.6", - "aws-cdk": "^2.136.0", + "@types/node": "20.12.7", + "aws-cdk": "^2.137.0", "constructs": "^10.3.0", "esbuild": "^0.20.2", "jest": "^29.7.0", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", - "typescript": "^5.4.4" + "typescript": "^5.4.5" } } diff --git a/layers/CHANGELOG.md b/layers/CHANGELOG.md index 1ab035b32b..68a75b0eb3 100644 --- a/layers/CHANGELOG.md +++ b/layers/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.1.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.0.4...v2.1.0) (2024-04-17) + + +### Features + +* **idempotency:** add custom JMESPath functions ([#2364](https://github.com/aws-powertools/powertools-lambda-typescript/issues/2364)) ([9721e7c](https://github.com/aws-powertools/powertools-lambda-typescript/commit/9721e7c01fc010944eb477bdbc24b9e06a5c4571)) + + + + + ## 2.0.4 (2024-04-10) **Note:** Version bump only for package layers diff --git a/layers/package.json b/layers/package.json index 39ff19ba48..77b45b34f3 100644 --- a/layers/package.json +++ b/layers/package.json @@ -1,6 +1,6 @@ { "name": "layers", - "version": "2.0.4", + "version": "2.1.0", "bin": { "layer": "bin/layers.js" }, @@ -39,8 +39,8 @@ "source-map-support": "^0.5.21" }, "dependencies": { - "aws-cdk": "^2.136.0", - "aws-cdk-lib": "^2.136.0", + "aws-cdk": "^2.137.0", + "aws-cdk-lib": "^2.137.0", "esbuild": "^0.20.2" } } diff --git a/layers/src/layer-publisher-stack.ts b/layers/src/layer-publisher-stack.ts index b2bb6f90e6..653a157a7a 100644 --- a/layers/src/layer-publisher-stack.ts +++ b/layers/src/layer-publisher-stack.ts @@ -62,6 +62,7 @@ export class LayerPublisherStack extends Stack { // the name is the same as the npm workspace name const utilities = [ 'commons', + 'jmespath', 'logger', 'metrics', 'tracer', @@ -87,8 +88,6 @@ export class LayerPublisherStack extends Stack { 'node_modules/async-hook-jl/test', 'node_modules/stack-chain/test', 'node_modules/shimmer/test', - 'node_modules/jmespath/artifacts', - 'node_modules/jmespath/bower.json', 'node_modules/obliterator/*.d.ts', 'node_modules/strnum/.vscode', 'node_modules/strnum/*.test.js', @@ -101,9 +100,7 @@ export class LayerPublisherStack extends Stack { 'node_modules/@aws-lambda-powertools/*/lib/**/*.d.ts', 'node_modules/@aws-lambda-powertools/*/lib/**/*.d.ts.map', 'node_modules/@aws-sdk/*/dist-types', - 'node_modules/@aws-sdk/*/dist-es', 'node_modules/@smithy/*/dist-types', - 'node_modules/@smithy/*/dist-es', 'node_modules/@smithy/**/README.md ', 'node_modules/@aws-sdk/**/README.md ', ]; diff --git a/layers/tests/e2e/layerPublisher.test.ts b/layers/tests/e2e/layerPublisher.test.ts index 9f5535c9c0..821fdd7a13 100644 --- a/layers/tests/e2e/layerPublisher.test.ts +++ b/layers/tests/e2e/layerPublisher.test.ts @@ -11,27 +11,36 @@ import { TestInvocationLogs, invokeFunctionOnce, generateTestUniqueName, + getRuntimeKey, } from '@aws-lambda-powertools/testing-utils'; import { TestNodejsFunction } from '@aws-lambda-powertools/testing-utils/resources/lambda'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, } from './constants'; import { join } from 'node:path'; import packageJson from '../../package.json'; jest.spyOn(console, 'log').mockImplementation(); +// eslint-disable-next-line func-style -- type assertions can't be arrow functions +function assertLogs( + logs: TestInvocationLogs | undefined +): asserts logs is TestInvocationLogs { + if (!logs) { + throw new Error('Function logs are not available'); + } +} + /** * This test has two stacks: * 1. LayerPublisherStack - publishes a layer version using the LayerPublisher construct and containing the Powertools utilities from the repo - * 2. TestStack - uses the layer published in the first stack and contains a lambda function that uses the Powertools utilities from the layer + * 2. TestStack - uses the layer published in the first stack and contains two lambda functions that use the Powertools utilities from the layer * * The lambda function is invoked once and the logs are collected. The goal of the test is to verify that the layer creation and usage works as expected. */ -describe(`Layers E2E tests, publisher stack`, () => { +describe(`Layers E2E tests`, () => { const testStack = new TestStack({ stackNameProps: { stackNamePrefix: RESOURCE_NAME_PREFIX, @@ -39,7 +48,15 @@ describe(`Layers E2E tests, publisher stack`, () => { }, }); - let invocationLogs: TestInvocationLogs; + /** + * Node.js 16.x does not support importing ESM modules from Lambda Layers reliably. + * + * The feature is available in Node.js 18.x and later. + * @see https://aws.amazon.com/blogs/compute/node-js-18-x-runtime-now-available-in-aws-lambda/ + */ + const cases = getRuntimeKey() === 'nodejs16x' ? ['CJS'] : ['CJS', 'ESM']; + const invocationLogsMap: Map<(typeof cases)[number], TestInvocationLogs> = + new Map(); const ssmParameterLayerName = generateTestUniqueName({ testPrefix: `${RESOURCE_NAME_PREFIX}`, @@ -75,76 +92,83 @@ describe(`Layers E2E tests, publisher stack`, () => { }); beforeAll(async () => { + // Deploy the stack that publishes the layer await testLayerStack.deploy(); + // Import the layer version from the stack outputs into the test stack const layerVersion = LayerVersion.fromLayerVersionArn( testStack.stack, 'LayerVersionArnReference', testLayerStack.findAndGetStackOutputValue('LatestLayerArn') ); - new TestNodejsFunction( - testStack, - { - entry: lambdaFunctionCodeFilePath, - environment: { - LAYERS_PATH: '/opt/nodejs/node_modules', - POWERTOOLS_PACKAGE_VERSION: powerToolsPackageVersion, - POWERTOOLS_SERVICE_NAME: 'LayerPublisherStack', - }, - bundling: { - externalModules: [ - '@aws-lambda-powertools/commons', - '@aws-lambda-powertools/logger', - '@aws-lambda-powertools/metrics', - '@aws-lambda-powertools/tracer', - '@aws-lambda-powertools/parameter', - '@aws-lambda-powertools/idempotency', - '@aws-lambda-powertools/batch', - ], + + // Add a lambda function for each output format to the test stack + cases.forEach((outputFormat) => { + new TestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + LAYERS_PATH: '/opt/nodejs/node_modules', + POWERTOOLS_PACKAGE_VERSION: powerToolsPackageVersion, + POWERTOOLS_SERVICE_NAME: 'LayerPublisherStack', + }, + bundling: { + externalModules: [ + '@aws-lambda-powertools/*', + '@aws-sdk/*', + 'aws-xray-sdk-node', + ], + }, + layers: [layerVersion], }, - layers: [layerVersion], - }, - { - nameSuffix: 'testFn', - } - ); + { + nameSuffix: `test${outputFormat}Fn`, + ...(outputFormat === 'ESM' && { outputFormat: 'ESM' }), + } + ); + }); + // Deploy the test stack await testStack.deploy(); - const functionName = testStack.findAndGetStackOutputValue('testFn'); - - invocationLogs = await invokeFunctionOnce({ - functionName, - }); + // Invoke the lambda function once for each output format and collect the logs + for await (const outputFormat of cases) { + invocationLogsMap.set( + outputFormat, + await invokeFunctionOnce({ + functionName: testStack.findAndGetStackOutputValue( + `test${outputFormat}Fn` + ), + }) + ); + } }, SETUP_TIMEOUT); - describe('package version and path check', () => { - it( - 'should have no errors in the logs, which indicates the pacakges version matches the expected one', - () => { + describe.each(cases)( + 'utilities tests for %s output format', + (outputFormat) => { + let invocationLogs: TestInvocationLogs; + beforeAll(() => { + const maybeInvocationLogs = invocationLogsMap.get(outputFormat); + assertLogs(maybeInvocationLogs); + invocationLogs = maybeInvocationLogs; + }); + + it('should have no errors in the logs, which indicates the pacakges version matches the expected one', () => { const logs = invocationLogs.getFunctionLogs('ERROR'); expect(logs.length).toBe(0); - }, - TEST_CASE_TIMEOUT - ); - }); + }); - describe('utilities usage', () => { - it( - 'should have one warning related to missing Metrics namespace', - () => { + it('should have one warning related to missing Metrics namespace', () => { const logs = invocationLogs.getFunctionLogs('WARN'); expect(logs.length).toBe(1); expect(logs[0]).toContain('Namespace should be defined, default used'); - }, - TEST_CASE_TIMEOUT - ); + }); - it( - 'should have one info log related to coldstart metric', - () => { + it('should have one info log related to coldstart metric', () => { const logs = invocationLogs.getFunctionLogs(); const emfLogEntry = logs.find((log) => log.match( @@ -153,13 +177,9 @@ describe(`Layers E2E tests, publisher stack`, () => { ); expect(emfLogEntry).toBeDefined(); - }, - TEST_CASE_TIMEOUT - ); + }); - it( - 'should have one debug log with tracer subsegment info', - () => { + it('should have one debug log with tracer subsegment info', () => { const logs = invocationLogs.getFunctionLogs('DEBUG'); expect(logs.length).toBe(1); @@ -182,10 +202,9 @@ describe(`Layers E2E tests, publisher stack`, () => { trace_id: traceIdFromLog, }) ); - }, - TEST_CASE_TIMEOUT - ); - }); + }); + } + ); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { diff --git a/lerna.json b/lerna.json index 5ca4ae6739..84424e6cab 100644 --- a/lerna.json +++ b/lerna.json @@ -9,11 +9,12 @@ "packages/batch", "packages/testing", "packages/jmespath", + "packages/parser", "examples/app", "layers", "docs/snippets" ], - "version": "2.0.4", + "version": "2.1.0", "npmClient": "npm", "message": "chore(release): %s [skip ci]" } diff --git a/mkdocs.yml b/mkdocs.yml index 85d4bfe511..ebc9137256 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,6 +20,7 @@ nav: - utilities/idempotency.md - utilities/batch.md - utilities/jmespath.md + - utilities/parser.md - Processes: - Roadmap: roadmap.md - Versioning policy: versioning.md @@ -125,3 +126,5 @@ extra: - icon: fontawesome/brands/discord link: https://discord.gg/B8zZKbbyET name: Join our Discord Server! + status: + new: New Utility diff --git a/package-lock.json b/package-lock.json index 8d7b06ebdb..494a78121a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "workspaces": [ "packages/commons", + "packages/jmespath", "packages/logger", "packages/metrics", "packages/tracer", @@ -17,7 +18,7 @@ "packages/idempotency", "packages/batch", "packages/testing", - "packages/jmespath", + "packages/parser", "docs/snippets", "layers", "examples/app" @@ -26,7 +27,7 @@ "@middy/core": "^4.7.0", "@types/aws-lambda": "^8.10.137", "@types/jest": "^29.5.12", - "@types/node": "^20.12.6", + "@types/node": "^20.12.7", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "eslint": "^8.57.0", @@ -45,7 +46,7 @@ "ts-node": "^10.9.2", "typedoc": "^0.25.13", "typedoc-plugin-missing-exports": "^2.2.0", - "typescript": "^5.4.4" + "typescript": "^5.4.5" }, "engines": { "node": ">=16" @@ -53,66 +54,68 @@ }, "docs/snippets": { "name": "docs", - "version": "2.0.4", + "version": "2.1.0", "license": "MIT-0", "devDependencies": { - "@aws-sdk/client-appconfigdata": "^3.549.0", - "@aws-sdk/client-dynamodb": "^3.549.0", - "@aws-sdk/client-secrets-manager": "^3.549.0", - "@aws-sdk/client-ssm": "^3.549.0", - "@aws-sdk/util-dynamodb": "^3.549.0", - "aws-sdk": "^2.1595.0", + "@aws-sdk/client-appconfigdata": "^3.554.0", + "@aws-sdk/client-dynamodb": "^3.554.0", + "@aws-sdk/client-secrets-manager": "^3.554.0", + "@aws-sdk/client-ssm": "^3.554.0", + "@aws-sdk/util-dynamodb": "^3.554.0", + "@middy/core": "^4.7.0", + "aws-sdk": "^2.1599.0", "aws-sdk-client-mock": "^4.0.0", "aws-sdk-client-mock-jest": "^4.0.0", "axios": "^1.6.8", - "hashi-vault-js": "^0.4.14" + "hashi-vault-js": "^0.4.14", + "zod": "^3.22.4" } }, "examples/app": { "name": "powertools-sample-app", - "version": "2.0.4", + "version": "2.1.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/batch": "^2.0.4", - "@aws-lambda-powertools/idempotency": "^2.0.4", - "@aws-lambda-powertools/logger": "^2.0.4", - "@aws-lambda-powertools/metrics": "^2.0.4", - "@aws-lambda-powertools/parameters": "^2.0.4", - "@aws-lambda-powertools/tracer": "^2.0.4", - "@aws-sdk/client-ssm": "^3.549.0", - "@aws-sdk/lib-dynamodb": "^3.549.0", + "@aws-lambda-powertools/batch": "^2.1.0", + "@aws-lambda-powertools/idempotency": "^2.1.0", + "@aws-lambda-powertools/logger": "^2.1.0", + "@aws-lambda-powertools/metrics": "^2.1.0", + "@aws-lambda-powertools/parameters": "^2.1.0", + "@aws-lambda-powertools/tracer": "^2.1.0", + "@aws-sdk/client-ssm": "^3.554.0", + "@aws-sdk/lib-dynamodb": "^3.554.0", "@middy/core": "^4.7.0", "@types/aws-lambda": "^8.10.137", "@types/jest": "^29.5.12", - "@types/node": "20.12.6", - "aws-cdk": "^2.136.0", + "@types/node": "20.12.7", + "aws-cdk": "^2.137.0", "constructs": "^10.3.0", "esbuild": "^0.20.2", "jest": "^29.7.0", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", - "typescript": "^5.4.4" + "typescript": "^5.4.5" }, "devDependencies": { "@types/aws-lambda": "^8.10.137", "@types/jest": "^29.5.12", - "@types/node": "20.12.6", - "aws-cdk": "^2.136.0", - "aws-cdk-lib": "^2.136.0", + "@types/node": "20.12.7", + "aws-cdk": "^2.137.0", + "aws-cdk-lib": "^2.137.0", "constructs": "^10.3.0", "jest": "^29.7.0", "source-map-support": "^0.5.21", "ts-jest": "^29.1.2", "tsx": "^4.7.2", - "typescript": "^5.4.4" + "typescript": "^5.4.5" } }, "layers": { - "version": "2.0.4", + "version": "2.1.0", "license": "MIT-0", "dependencies": { - "aws-cdk": "^2.136.0", - "aws-cdk-lib": "^2.136.0", + "aws-cdk": "^2.137.0", + "aws-cdk-lib": "^2.137.0", "esbuild": "^0.20.2" }, "bin": { @@ -145,6 +148,19 @@ "node": ">=6.0.0" } }, + "node_modules/@anatine/zod-mock": { + "version": "3.13.4", + "resolved": "https://registry.npmjs.org/@anatine/zod-mock/-/zod-mock-3.13.4.tgz", + "integrity": "sha512-yO/KeuyYsEDCTcQ+7CiRuY3dnafMHIZUMok6Ci7aERRCTQ+/XmsiPk/RnMx5wlLmWBTmX9kw+PavbMsjM+sAJA==", + "dev": true, + "dependencies": { + "randexp": "^0.5.3" + }, + "peerDependencies": { + "@faker-js/faker": "^7.0.0 || ^8.0.0", + "zod": "^3.21.4" + } + }, "node_modules/@aws-cdk/asset-awscli-v1": { "version": "2.2.202", "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.202.tgz", @@ -287,6 +303,10 @@ "resolved": "packages/parameters", "link": true }, + "node_modules/@aws-lambda-powertools/parser": { + "resolved": "packages/parser", + "link": true + }, "node_modules/@aws-lambda-powertools/testing-utils": { "resolved": "packages/testing", "link": true @@ -296,16 +316,16 @@ "link": true }, "node_modules/@aws-sdk/client-appconfigdata": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-appconfigdata/-/client-appconfigdata-3.549.0.tgz", - "integrity": "sha512-J71f6UzbOcor6IQGkCHPLR7CH7M5qUPalAhZtBOc7DJApVQbO9bcG+bBqGSnxTY6jSeUih42hVRcij0v/2qanw==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-appconfigdata/-/client-appconfigdata-3.554.0.tgz", + "integrity": "sha512-8EuPZ8r9eBfUv7NTE8c9e0MFfL49XY7sjd8hoFpxeDBdMcrYRbQRORiV36Hl8eh6nkeqebCQnbq5jvhglZ1KjA==", "dev": true, "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.549.0", - "@aws-sdk/core": "3.549.0", - "@aws-sdk/credential-provider-node": "3.549.0", + "@aws-sdk/client-sts": "3.554.0", + "@aws-sdk/core": "3.554.0", + "@aws-sdk/credential-provider-node": "3.554.0", "@aws-sdk/middleware-host-header": "3.535.0", "@aws-sdk/middleware-logger": "3.535.0", "@aws-sdk/middleware-recursion-detection": "3.535.0", @@ -316,26 +336,26 @@ "@aws-sdk/util-user-agent-browser": "3.535.0", "@aws-sdk/util-user-agent-node": "3.535.0", "@smithy/config-resolver": "^2.2.0", - "@smithy/core": "^1.4.1", + "@smithy/core": "^1.4.2", "@smithy/fetch-http-handler": "^2.5.0", "@smithy/hash-node": "^2.2.0", "@smithy/invalid-dependency": "^2.2.0", "@smithy/middleware-content-length": "^2.2.0", - "@smithy/middleware-endpoint": "^2.5.0", - "@smithy/middleware-retry": "^2.3.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", "@smithy/middleware-serde": "^2.3.0", "@smithy/middleware-stack": "^2.2.0", "@smithy/node-config-provider": "^2.3.0", "@smithy/node-http-handler": "^2.5.0", "@smithy/protocol-http": "^3.3.0", - "@smithy/smithy-client": "^2.5.0", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "@smithy/url-parser": "^2.2.0", "@smithy/util-base64": "^2.3.0", "@smithy/util-body-length-browser": "^2.2.0", "@smithy/util-body-length-node": "^2.3.0", - "@smithy/util-defaults-mode-browser": "^2.2.0", - "@smithy/util-defaults-mode-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", "@smithy/util-endpoints": "^1.2.0", "@smithy/util-middleware": "^2.2.0", "@smithy/util-retry": "^2.2.0", @@ -348,16 +368,16 @@ } }, "node_modules/@aws-sdk/client-cloudwatch": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.549.0.tgz", - "integrity": "sha512-b/+JmCAdo9L2GxMeU0FANdTFN/mYyq7vV380P6qyvDdQjtd/pgKjz9iE1itqEhWPRkhzIzO7iyqS/d3BSFC/vA==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.554.0.tgz", + "integrity": "sha512-8MqmsbR+UcF+qQQWFV/wCr1Q7YFg1r0v6rg5fxBRJigmc3Xp63m5L3zL3/+Cdf7qkDbXw6BGdk+yclfoPRN1RQ==", "dev": true, "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.549.0", - "@aws-sdk/core": "3.549.0", - "@aws-sdk/credential-provider-node": "3.549.0", + "@aws-sdk/client-sts": "3.554.0", + "@aws-sdk/core": "3.554.0", + "@aws-sdk/credential-provider-node": "3.554.0", "@aws-sdk/middleware-host-header": "3.535.0", "@aws-sdk/middleware-logger": "3.535.0", "@aws-sdk/middleware-recursion-detection": "3.535.0", @@ -368,27 +388,27 @@ "@aws-sdk/util-user-agent-browser": "3.535.0", "@aws-sdk/util-user-agent-node": "3.535.0", "@smithy/config-resolver": "^2.2.0", - "@smithy/core": "^1.4.1", + "@smithy/core": "^1.4.2", "@smithy/fetch-http-handler": "^2.5.0", "@smithy/hash-node": "^2.2.0", "@smithy/invalid-dependency": "^2.2.0", "@smithy/middleware-compression": "^2.2.0", "@smithy/middleware-content-length": "^2.2.0", - "@smithy/middleware-endpoint": "^2.5.0", - "@smithy/middleware-retry": "^2.3.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", "@smithy/middleware-serde": "^2.3.0", "@smithy/middleware-stack": "^2.2.0", "@smithy/node-config-provider": "^2.3.0", "@smithy/node-http-handler": "^2.5.0", "@smithy/protocol-http": "^3.3.0", - "@smithy/smithy-client": "^2.5.0", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "@smithy/url-parser": "^2.2.0", "@smithy/util-base64": "^2.3.0", "@smithy/util-body-length-browser": "^2.2.0", "@smithy/util-body-length-node": "^2.3.0", - "@smithy/util-defaults-mode-browser": "^2.2.0", - "@smithy/util-defaults-mode-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", "@smithy/util-endpoints": "^1.2.0", "@smithy/util-middleware": "^2.2.0", "@smithy/util-retry": "^2.2.0", @@ -401,15 +421,15 @@ } }, "node_modules/@aws-sdk/client-dynamodb": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.549.0.tgz", - "integrity": "sha512-0pwxpR7AuoRk+K6l2G+/jVSaALcFDt3b8EC0mEU0WhLZoWIS4Zvue3RUbnwEMyKckydO2DgYshsCvSSmLDjlcA==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.554.0.tgz", + "integrity": "sha512-w1Bepf3r8tTSymBiXqDHfzDc7J9b0sRbQeRtFUmCI9HFiQkLhDigGBkorNkJohCzwO1Pxh6Z4/a0mWZ7d8f1Zw==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.549.0", - "@aws-sdk/core": "3.549.0", - "@aws-sdk/credential-provider-node": "3.549.0", + "@aws-sdk/client-sts": "3.554.0", + "@aws-sdk/core": "3.554.0", + "@aws-sdk/credential-provider-node": "3.554.0", "@aws-sdk/middleware-endpoint-discovery": "3.535.0", "@aws-sdk/middleware-host-header": "3.535.0", "@aws-sdk/middleware-logger": "3.535.0", @@ -421,26 +441,26 @@ "@aws-sdk/util-user-agent-browser": "3.535.0", "@aws-sdk/util-user-agent-node": "3.535.0", "@smithy/config-resolver": "^2.2.0", - "@smithy/core": "^1.4.1", + "@smithy/core": "^1.4.2", "@smithy/fetch-http-handler": "^2.5.0", "@smithy/hash-node": "^2.2.0", "@smithy/invalid-dependency": "^2.2.0", "@smithy/middleware-content-length": "^2.2.0", - "@smithy/middleware-endpoint": "^2.5.0", - "@smithy/middleware-retry": "^2.3.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", "@smithy/middleware-serde": "^2.3.0", "@smithy/middleware-stack": "^2.2.0", "@smithy/node-config-provider": "^2.3.0", "@smithy/node-http-handler": "^2.5.0", "@smithy/protocol-http": "^3.3.0", - "@smithy/smithy-client": "^2.5.0", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "@smithy/url-parser": "^2.2.0", "@smithy/util-base64": "^2.3.0", "@smithy/util-body-length-browser": "^2.2.0", "@smithy/util-body-length-node": "^2.3.0", - "@smithy/util-defaults-mode-browser": "^2.2.0", - "@smithy/util-defaults-mode-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", "@smithy/util-endpoints": "^1.2.0", "@smithy/util-middleware": "^2.2.0", "@smithy/util-retry": "^2.2.0", @@ -454,15 +474,15 @@ } }, "node_modules/@aws-sdk/client-lambda": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.549.0.tgz", - "integrity": "sha512-VdLltPf6fUDBFHPJBtYsWnM+nvdau7KuRlwwGnYzmYap79ifE538JTXE6AGyGcIp3YP44BlFtWgvW26kvDbX9g==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.554.0.tgz", + "integrity": "sha512-KNUAAZKcsCdUOB2/rbWpc96jsSM/ahw3hK5/Ru4RTLfNP27GitxqF0v+mzrVk9lTuj2ChJ3JDV+UfdGsqvZgpw==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.549.0", - "@aws-sdk/core": "3.549.0", - "@aws-sdk/credential-provider-node": "3.549.0", + "@aws-sdk/client-sts": "3.554.0", + "@aws-sdk/core": "3.554.0", + "@aws-sdk/credential-provider-node": "3.554.0", "@aws-sdk/middleware-host-header": "3.535.0", "@aws-sdk/middleware-logger": "3.535.0", "@aws-sdk/middleware-recursion-detection": "3.535.0", @@ -473,7 +493,7 @@ "@aws-sdk/util-user-agent-browser": "3.535.0", "@aws-sdk/util-user-agent-node": "3.535.0", "@smithy/config-resolver": "^2.2.0", - "@smithy/core": "^1.4.1", + "@smithy/core": "^1.4.2", "@smithy/eventstream-serde-browser": "^2.2.0", "@smithy/eventstream-serde-config-resolver": "^2.2.0", "@smithy/eventstream-serde-node": "^2.2.0", @@ -481,21 +501,21 @@ "@smithy/hash-node": "^2.2.0", "@smithy/invalid-dependency": "^2.2.0", "@smithy/middleware-content-length": "^2.2.0", - "@smithy/middleware-endpoint": "^2.5.0", - "@smithy/middleware-retry": "^2.3.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", "@smithy/middleware-serde": "^2.3.0", "@smithy/middleware-stack": "^2.2.0", "@smithy/node-config-provider": "^2.3.0", "@smithy/node-http-handler": "^2.5.0", "@smithy/protocol-http": "^3.3.0", - "@smithy/smithy-client": "^2.5.0", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "@smithy/url-parser": "^2.2.0", "@smithy/util-base64": "^2.3.0", "@smithy/util-body-length-browser": "^2.2.0", "@smithy/util-body-length-node": "^2.3.0", - "@smithy/util-defaults-mode-browser": "^2.2.0", - "@smithy/util-defaults-mode-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", "@smithy/util-endpoints": "^1.2.0", "@smithy/util-middleware": "^2.2.0", "@smithy/util-retry": "^2.2.0", @@ -509,16 +529,16 @@ } }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.549.0.tgz", - "integrity": "sha512-UcfU1vdghAJMWK1T6NnNe0ZhOIfSY6s0OBooXSybDgZvH47g+rYD/4VdtZRWRSjdCcF4PA+hfRa7vhfOyXdW/A==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.554.0.tgz", + "integrity": "sha512-Uk9rdO6nP1Ayg6maOCD7ZI7QlRzDCGoFQtp/hxBt0uGro5C47Rpg5N6Wn3Lblk/rGnDcq+nuX24WXo83jOi/HQ==", "dev": true, "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.549.0", - "@aws-sdk/core": "3.549.0", - "@aws-sdk/credential-provider-node": "3.549.0", + "@aws-sdk/client-sts": "3.554.0", + "@aws-sdk/core": "3.554.0", + "@aws-sdk/credential-provider-node": "3.554.0", "@aws-sdk/middleware-host-header": "3.535.0", "@aws-sdk/middleware-logger": "3.535.0", "@aws-sdk/middleware-recursion-detection": "3.535.0", @@ -529,26 +549,26 @@ "@aws-sdk/util-user-agent-browser": "3.535.0", "@aws-sdk/util-user-agent-node": "3.535.0", "@smithy/config-resolver": "^2.2.0", - "@smithy/core": "^1.4.1", + "@smithy/core": "^1.4.2", "@smithy/fetch-http-handler": "^2.5.0", "@smithy/hash-node": "^2.2.0", "@smithy/invalid-dependency": "^2.2.0", "@smithy/middleware-content-length": "^2.2.0", - "@smithy/middleware-endpoint": "^2.5.0", - "@smithy/middleware-retry": "^2.3.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", "@smithy/middleware-serde": "^2.3.0", "@smithy/middleware-stack": "^2.2.0", "@smithy/node-config-provider": "^2.3.0", "@smithy/node-http-handler": "^2.5.0", "@smithy/protocol-http": "^3.3.0", - "@smithy/smithy-client": "^2.5.0", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "@smithy/url-parser": "^2.2.0", "@smithy/util-base64": "^2.3.0", "@smithy/util-body-length-browser": "^2.2.0", "@smithy/util-body-length-node": "^2.3.0", - "@smithy/util-defaults-mode-browser": "^2.2.0", - "@smithy/util-defaults-mode-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", "@smithy/util-endpoints": "^1.2.0", "@smithy/util-middleware": "^2.2.0", "@smithy/util-retry": "^2.2.0", @@ -561,15 +581,15 @@ } }, "node_modules/@aws-sdk/client-ssm": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.549.0.tgz", - "integrity": "sha512-h5EvM1e09+ybmaCFXVjibWGyBSJ7yNIgKFq1SoifnZ5O4nf6SNNAhbuea1Gq4JCcjvDfzJ4Wl3EqVHf/Jb9oJg==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.554.0.tgz", + "integrity": "sha512-zqc5Pyb0agJ3erp1x2ILoll7mG6atQTD2AFWA5UBFhNa7R0+w+TLvSNnX813X4bv4OySqBYYEtAokoTvV66UZw==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.549.0", - "@aws-sdk/core": "3.549.0", - "@aws-sdk/credential-provider-node": "3.549.0", + "@aws-sdk/client-sts": "3.554.0", + "@aws-sdk/core": "3.554.0", + "@aws-sdk/credential-provider-node": "3.554.0", "@aws-sdk/middleware-host-header": "3.535.0", "@aws-sdk/middleware-logger": "3.535.0", "@aws-sdk/middleware-recursion-detection": "3.535.0", @@ -580,26 +600,26 @@ "@aws-sdk/util-user-agent-browser": "3.535.0", "@aws-sdk/util-user-agent-node": "3.535.0", "@smithy/config-resolver": "^2.2.0", - "@smithy/core": "^1.4.1", + "@smithy/core": "^1.4.2", "@smithy/fetch-http-handler": "^2.5.0", "@smithy/hash-node": "^2.2.0", "@smithy/invalid-dependency": "^2.2.0", "@smithy/middleware-content-length": "^2.2.0", - "@smithy/middleware-endpoint": "^2.5.0", - "@smithy/middleware-retry": "^2.3.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", "@smithy/middleware-serde": "^2.3.0", "@smithy/middleware-stack": "^2.2.0", "@smithy/node-config-provider": "^2.3.0", "@smithy/node-http-handler": "^2.5.0", "@smithy/protocol-http": "^3.3.0", - "@smithy/smithy-client": "^2.5.0", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "@smithy/url-parser": "^2.2.0", "@smithy/util-base64": "^2.3.0", "@smithy/util-body-length-browser": "^2.2.0", "@smithy/util-body-length-node": "^2.3.0", - "@smithy/util-defaults-mode-browser": "^2.2.0", - "@smithy/util-defaults-mode-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", "@smithy/util-endpoints": "^1.2.0", "@smithy/util-middleware": "^2.2.0", "@smithy/util-retry": "^2.2.0", @@ -613,13 +633,13 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.549.0.tgz", - "integrity": "sha512-lz+yflOAj5Q263FlCsKpNqttaCb2NPh8jC76gVCqCt7TPxRDBYVaqg0OZYluDaETIDNJi4DwN2Azcck7ilwuPw==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.554.0.tgz", + "integrity": "sha512-yj6CgIxCT3UwMumEO481KH4QvwArkAPzD7Xvwe1QKgJATc9bKNEo/FxV8LfnWIJ7nOtMDxbNxYLMXH/Fs1qGaQ==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/core": "3.549.0", + "@aws-sdk/core": "3.554.0", "@aws-sdk/middleware-host-header": "3.535.0", "@aws-sdk/middleware-logger": "3.535.0", "@aws-sdk/middleware-recursion-detection": "3.535.0", @@ -630,26 +650,26 @@ "@aws-sdk/util-user-agent-browser": "3.535.0", "@aws-sdk/util-user-agent-node": "3.535.0", "@smithy/config-resolver": "^2.2.0", - "@smithy/core": "^1.4.1", + "@smithy/core": "^1.4.2", "@smithy/fetch-http-handler": "^2.5.0", "@smithy/hash-node": "^2.2.0", "@smithy/invalid-dependency": "^2.2.0", "@smithy/middleware-content-length": "^2.2.0", - "@smithy/middleware-endpoint": "^2.5.0", - "@smithy/middleware-retry": "^2.3.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", "@smithy/middleware-serde": "^2.3.0", "@smithy/middleware-stack": "^2.2.0", "@smithy/node-config-provider": "^2.3.0", "@smithy/node-http-handler": "^2.5.0", "@smithy/protocol-http": "^3.3.0", - "@smithy/smithy-client": "^2.5.0", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "@smithy/url-parser": "^2.2.0", "@smithy/util-base64": "^2.3.0", "@smithy/util-body-length-browser": "^2.2.0", "@smithy/util-body-length-node": "^2.3.0", - "@smithy/util-defaults-mode-browser": "^2.2.0", - "@smithy/util-defaults-mode-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", "@smithy/util-endpoints": "^1.2.0", "@smithy/util-middleware": "^2.2.0", "@smithy/util-retry": "^2.2.0", @@ -661,14 +681,14 @@ } }, "node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.549.0.tgz", - "integrity": "sha512-FbB4A78ILAb8sM4TfBd+3CrQcfZIhe0gtVZNbaxpq5cJZh1K7oZ8vPfKw4do9JWkDUXPLsD9Bwz12f8/JpAb6Q==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.554.0.tgz", + "integrity": "sha512-M86rkiRqbZBF5VyfTQ/vttry9VSoQkZ1oCqYF+SAGlXmD0Of8587yRSj2M4rYe0Uj7nRQIfSnhDYp1UzsZeRfQ==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.549.0", - "@aws-sdk/core": "3.549.0", + "@aws-sdk/client-sts": "3.554.0", + "@aws-sdk/core": "3.554.0", "@aws-sdk/middleware-host-header": "3.535.0", "@aws-sdk/middleware-logger": "3.535.0", "@aws-sdk/middleware-recursion-detection": "3.535.0", @@ -679,26 +699,26 @@ "@aws-sdk/util-user-agent-browser": "3.535.0", "@aws-sdk/util-user-agent-node": "3.535.0", "@smithy/config-resolver": "^2.2.0", - "@smithy/core": "^1.4.1", + "@smithy/core": "^1.4.2", "@smithy/fetch-http-handler": "^2.5.0", "@smithy/hash-node": "^2.2.0", "@smithy/invalid-dependency": "^2.2.0", "@smithy/middleware-content-length": "^2.2.0", - "@smithy/middleware-endpoint": "^2.5.0", - "@smithy/middleware-retry": "^2.3.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", "@smithy/middleware-serde": "^2.3.0", "@smithy/middleware-stack": "^2.2.0", "@smithy/node-config-provider": "^2.3.0", "@smithy/node-http-handler": "^2.5.0", "@smithy/protocol-http": "^3.3.0", - "@smithy/smithy-client": "^2.5.0", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "@smithy/url-parser": "^2.2.0", "@smithy/util-base64": "^2.3.0", "@smithy/util-body-length-browser": "^2.2.0", "@smithy/util-body-length-node": "^2.3.0", - "@smithy/util-defaults-mode-browser": "^2.2.0", - "@smithy/util-defaults-mode-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", "@smithy/util-endpoints": "^1.2.0", "@smithy/util-middleware": "^2.2.0", "@smithy/util-retry": "^2.2.0", @@ -709,17 +729,17 @@ "node": ">=14.0.0" }, "peerDependencies": { - "@aws-sdk/credential-provider-node": "^3.549.0" + "@aws-sdk/credential-provider-node": "^3.554.0" } }, "node_modules/@aws-sdk/client-sts": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.549.0.tgz", - "integrity": "sha512-63IreJ598Dzvpb+6sy81KfIX5iQxnrWSEtlyeCdC2GO6gmSQVwJzc9kr5pAC83lHmlZcm/Q3KZr3XBhRQqP0og==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.554.0.tgz", + "integrity": "sha512-EhaA6T0M0DNg5M8TCF1a7XJI5D/ZxAF3dgVIchyF98iNzjYgl/7U8K6hJay2A11aFvVu70g46xYMpz3Meky4wQ==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/core": "3.549.0", + "@aws-sdk/core": "3.554.0", "@aws-sdk/middleware-host-header": "3.535.0", "@aws-sdk/middleware-logger": "3.535.0", "@aws-sdk/middleware-recursion-detection": "3.535.0", @@ -730,26 +750,26 @@ "@aws-sdk/util-user-agent-browser": "3.535.0", "@aws-sdk/util-user-agent-node": "3.535.0", "@smithy/config-resolver": "^2.2.0", - "@smithy/core": "^1.4.1", + "@smithy/core": "^1.4.2", "@smithy/fetch-http-handler": "^2.5.0", "@smithy/hash-node": "^2.2.0", "@smithy/invalid-dependency": "^2.2.0", "@smithy/middleware-content-length": "^2.2.0", - "@smithy/middleware-endpoint": "^2.5.0", - "@smithy/middleware-retry": "^2.3.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", "@smithy/middleware-serde": "^2.3.0", "@smithy/middleware-stack": "^2.2.0", "@smithy/node-config-provider": "^2.3.0", "@smithy/node-http-handler": "^2.5.0", "@smithy/protocol-http": "^3.3.0", - "@smithy/smithy-client": "^2.5.0", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "@smithy/url-parser": "^2.2.0", "@smithy/util-base64": "^2.3.0", "@smithy/util-body-length-browser": "^2.2.0", "@smithy/util-body-length-node": "^2.3.0", - "@smithy/util-defaults-mode-browser": "^2.2.0", - "@smithy/util-defaults-mode-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", "@smithy/util-endpoints": "^1.2.0", "@smithy/util-middleware": "^2.2.0", "@smithy/util-retry": "^2.2.0", @@ -760,20 +780,20 @@ "node": ">=14.0.0" }, "peerDependencies": { - "@aws-sdk/credential-provider-node": "^3.549.0" + "@aws-sdk/credential-provider-node": "^3.554.0" } }, "node_modules/@aws-sdk/client-xray": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-xray/-/client-xray-3.549.0.tgz", - "integrity": "sha512-pI5gJ79HZUAoRFmtUB5sLaX2mopKzbGor/IQqYpUMwa61RKPOozlTOyLr+lFItbt9dpYoI4QiKGPfo5RAbYpqw==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-xray/-/client-xray-3.554.0.tgz", + "integrity": "sha512-9wtvYKS+5XCG6jHEGVNsDZzpmOBCiLj+scpblcywhcKooi6vFj4LafbM2SyAufTKhUKksio90Ka16yHQf2MX9g==", "dev": true, "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.549.0", - "@aws-sdk/core": "3.549.0", - "@aws-sdk/credential-provider-node": "3.549.0", + "@aws-sdk/client-sts": "3.554.0", + "@aws-sdk/core": "3.554.0", + "@aws-sdk/credential-provider-node": "3.554.0", "@aws-sdk/middleware-host-header": "3.535.0", "@aws-sdk/middleware-logger": "3.535.0", "@aws-sdk/middleware-recursion-detection": "3.535.0", @@ -784,26 +804,26 @@ "@aws-sdk/util-user-agent-browser": "3.535.0", "@aws-sdk/util-user-agent-node": "3.535.0", "@smithy/config-resolver": "^2.2.0", - "@smithy/core": "^1.4.1", + "@smithy/core": "^1.4.2", "@smithy/fetch-http-handler": "^2.5.0", "@smithy/hash-node": "^2.2.0", "@smithy/invalid-dependency": "^2.2.0", "@smithy/middleware-content-length": "^2.2.0", - "@smithy/middleware-endpoint": "^2.5.0", - "@smithy/middleware-retry": "^2.3.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", "@smithy/middleware-serde": "^2.3.0", "@smithy/middleware-stack": "^2.2.0", "@smithy/node-config-provider": "^2.3.0", "@smithy/node-http-handler": "^2.5.0", "@smithy/protocol-http": "^3.3.0", - "@smithy/smithy-client": "^2.5.0", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "@smithy/url-parser": "^2.2.0", "@smithy/util-base64": "^2.3.0", "@smithy/util-body-length-browser": "^2.2.0", "@smithy/util-body-length-node": "^2.3.0", - "@smithy/util-defaults-mode-browser": "^2.2.0", - "@smithy/util-defaults-mode-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", "@smithy/util-endpoints": "^1.2.0", "@smithy/util-middleware": "^2.2.0", "@smithy/util-retry": "^2.2.0", @@ -815,14 +835,14 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.549.0.tgz", - "integrity": "sha512-jC61OxJn72r/BbuDRCcluiw05Xw9eVLG0CwxQpF3RocxfxyZqlrGYaGecZ8Wy+7g/3sqGRC/Ar5eUhU1YcLx7w==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.554.0.tgz", + "integrity": "sha512-JrG7ToTLeNf+/S3IiCUPVw9jEDB0DXl5ho8n/HwOa946mv+QyCepCuV2U/8f/1KAX0mD8Ufm/E4/cbCbFHgbSg==", "dependencies": { - "@smithy/core": "^1.4.1", + "@smithy/core": "^1.4.2", "@smithy/protocol-http": "^3.3.0", - "@smithy/signature-v4": "^2.2.0", - "@smithy/smithy-client": "^2.5.0", + "@smithy/signature-v4": "^2.2.1", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "fast-xml-parser": "4.2.5", "tslib": "^2.6.2" @@ -846,16 +866,16 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.535.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.535.0.tgz", - "integrity": "sha512-kdj1wCmOMZ29jSlUskRqN04S6fJ4dvt0Nq9Z32SA6wO7UG8ht6Ot9h/au/eTWJM3E1somZ7D771oK7dQt9b8yw==", + "version": "3.552.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.552.0.tgz", + "integrity": "sha512-vsmu7Cz1i45pFEqzVb4JcFmAmVnWFNLsGheZc8SCptlqCO5voETrZZILHYIl4cjKkSDk3pblBOf0PhyjqWW6WQ==", "dependencies": { "@aws-sdk/types": "3.535.0", "@smithy/fetch-http-handler": "^2.5.0", "@smithy/node-http-handler": "^2.5.0", "@smithy/property-provider": "^2.2.0", "@smithy/protocol-http": "^3.3.0", - "@smithy/smithy-client": "^2.5.0", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "@smithy/util-stream": "^2.2.0", "tslib": "^2.6.2" @@ -865,15 +885,15 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.549.0.tgz", - "integrity": "sha512-k6IIrluZjQpzui5Din8fW3bFFhHaJ64XrsfYx0Ks1mb7xan84dJxmYP3tdDDmLzUeJv5h95ag88taHfjY9rakA==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.554.0.tgz", + "integrity": "sha512-BQenhg43S6TMJHxrdjDVdVF+HH5tA1op9ZYLyJrvV5nn7CCO4kyAkkOuSAv1NkL+RZsIkW0/vHTXwQOQw3cUsg==", "dependencies": { - "@aws-sdk/client-sts": "3.549.0", + "@aws-sdk/client-sts": "3.554.0", "@aws-sdk/credential-provider-env": "3.535.0", "@aws-sdk/credential-provider-process": "3.535.0", - "@aws-sdk/credential-provider-sso": "3.549.0", - "@aws-sdk/credential-provider-web-identity": "3.549.0", + "@aws-sdk/credential-provider-sso": "3.554.0", + "@aws-sdk/credential-provider-web-identity": "3.554.0", "@aws-sdk/types": "3.535.0", "@smithy/credential-provider-imds": "^2.3.0", "@smithy/property-provider": "^2.2.0", @@ -886,16 +906,16 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.549.0.tgz", - "integrity": "sha512-f3YgalsMuywEAVX4AUm9tojqrBdfpAac0+D320ePzas0Ntbp7ItYu9ceKIhgfzXO3No7P3QK0rCrOxL+ABTn8Q==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.554.0.tgz", + "integrity": "sha512-poX/+2OE3oxqp4f5MiaJh251p8l+bzcFwgcDBwz0e2rcpvMSYl9jw4AvGnCiG2bmf9yhNJdftBiS1A+KjxV0qA==", "dependencies": { "@aws-sdk/credential-provider-env": "3.535.0", - "@aws-sdk/credential-provider-http": "3.535.0", - "@aws-sdk/credential-provider-ini": "3.549.0", + "@aws-sdk/credential-provider-http": "3.552.0", + "@aws-sdk/credential-provider-ini": "3.554.0", "@aws-sdk/credential-provider-process": "3.535.0", - "@aws-sdk/credential-provider-sso": "3.549.0", - "@aws-sdk/credential-provider-web-identity": "3.549.0", + "@aws-sdk/credential-provider-sso": "3.554.0", + "@aws-sdk/credential-provider-web-identity": "3.554.0", "@aws-sdk/types": "3.535.0", "@smithy/credential-provider-imds": "^2.3.0", "@smithy/property-provider": "^2.2.0", @@ -923,12 +943,12 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.549.0.tgz", - "integrity": "sha512-BGopRKHs7W8zkoH8qmSHrjudj263kXbhVkAUPxVUz0I28+CZNBgJC/RfVCbOpzmysIQEpwSqvOv1y0k+DQzIJQ==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.554.0.tgz", + "integrity": "sha512-8QPpwBA31i/fZ7lDZJC4FA9EdxLg5SJ8sPB2qLSjp5UTGTYL2HRl0Eznkb7DXyp/wImsR/HFR1NxuFCCVotLCg==", "dependencies": { - "@aws-sdk/client-sso": "3.549.0", - "@aws-sdk/token-providers": "3.549.0", + "@aws-sdk/client-sso": "3.554.0", + "@aws-sdk/token-providers": "3.554.0", "@aws-sdk/types": "3.535.0", "@smithy/property-provider": "^2.2.0", "@smithy/shared-ini-file-loader": "^2.4.0", @@ -940,11 +960,11 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.549.0.tgz", - "integrity": "sha512-QzclVXPxuwSI7515l34sdvliVq5leroO8P7RQFKRgfyQKO45o1psghierwG3PgV6jlMiv78FIAGJBr/n4qZ7YA==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.554.0.tgz", + "integrity": "sha512-HN54DzLjepw5ZWSF9ycGevhFTyg6pjLuLKy5Y8t/f1jFDComzYdGEDe0cdV9YO653W3+PQwZZGz09YVygGYBLg==", "dependencies": { - "@aws-sdk/client-sts": "3.549.0", + "@aws-sdk/client-sts": "3.554.0", "@aws-sdk/types": "3.535.0", "@smithy/property-provider": "^2.2.0", "@smithy/types": "^2.12.0", @@ -967,12 +987,12 @@ } }, "node_modules/@aws-sdk/lib-dynamodb": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.549.0.tgz", - "integrity": "sha512-1TQhsjLRWuppbxqasONzURPlgMpif5mrICbUCqDIdUaN5bLnAWOoATT3Bqkdr3M33yEe90aNH9/8kFKaWq+K9A==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.554.0.tgz", + "integrity": "sha512-hbkKKkDf0lYB0lJi9at/iPI84X81vWz52YGl8ggEANksbFRTNgicxWPmiPw46T4E8FMKLTvEjPJU912xqgF0Eg==", "dependencies": { - "@aws-sdk/util-dynamodb": "3.549.0", - "@smithy/smithy-client": "^2.5.0", + "@aws-sdk/util-dynamodb": "3.554.0", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "tslib": "^2.6.2" }, @@ -1072,11 +1092,11 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.549.0.tgz", - "integrity": "sha512-rJyeXkXknLukRFGuMQOgKnPBa+kLODJtOqEBf929SpQ96f1I6ytdndmWbB5B/OQN5Fu5DOOQUQqJypDQVl5ibQ==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.554.0.tgz", + "integrity": "sha512-KMMQ5Cw0FUPL9H8g69Lp08xtzRo7r/MK+lBV6LznWBbCP/NwtZ8awVHaPy2P31z00cWtu9MYkUTviWPqJTaBvg==", "dependencies": { - "@aws-sdk/client-sso-oidc": "3.549.0", + "@aws-sdk/client-sso-oidc": "3.554.0", "@aws-sdk/types": "3.535.0", "@smithy/property-provider": "^2.2.0", "@smithy/shared-ini-file-loader": "^2.4.0", @@ -1100,9 +1120,9 @@ } }, "node_modules/@aws-sdk/util-dynamodb": { - "version": "3.549.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.549.0.tgz", - "integrity": "sha512-HvJmvDNw9GN1vxWjbM7jYqv+DvQVeL21kLQ+Tth24QRPA1tiCrUuBi+lerRZ8fHG/kiZMTGyJ7UOPvljGD84dw==", + "version": "3.554.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.554.0.tgz", + "integrity": "sha512-Q81Rj38Ban8iV0C1b0cOEHWVHveIW67zVyd2gJvn1xdeKz+N4zCCUviL9Dwfsi6YgOaP+eQaaCptsBA0Nrx7YA==", "dependencies": { "tslib": "^2.6.2" }, @@ -2271,6 +2291,22 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -4487,11 +4523,10 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.2.0.tgz", - "integrity": "sha512-+B5TNzj/fRZzVW3z8UUJOkNx15+4E0CLuvJmJUA1JUIZFp3rdJ/M2H5r2SqltaVPXL0oIxv/6YK92T9TsFGbFg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.2.1.tgz", + "integrity": "sha512-j5fHgL1iqKTsKJ1mTcw88p0RUcidDu95AWSeZTgiYJb+QcfwWU/UpBnaqiB59FNH5MiAZuSbOBnZlwzeeY2tIw==", "dependencies": { - "@smithy/eventstream-codec": "^2.2.0", "@smithy/is-array-buffer": "^2.2.0", "@smithy/types": "^2.12.0", "@smithy/util-hex-encoding": "^2.2.0", @@ -4597,12 +4632,12 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.2.0.tgz", - "integrity": "sha512-2okTdZaCBvOJszAPU/KSvlimMe35zLOKbQpHhamFJmR7t95HSe0K3C92jQPjKY3PmDBD+7iMkOnuW05F5OlF4g==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.2.1.tgz", + "integrity": "sha512-RtKW+8j8skk17SYowucwRUjeh4mCtnm5odCL0Lm2NtHQBsYKrNW0od9Rhopu9wF1gHMfHeWF7i90NwBz/U22Kw==", "dependencies": { "@smithy/property-provider": "^2.2.0", - "@smithy/smithy-client": "^2.5.0", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" @@ -4612,15 +4647,15 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.3.0.tgz", - "integrity": "sha512-hfKXnNLmsW9cmLb/JXKIvtuO6Cf4SuqN5PN1C2Ru/TBIws+m1wSgb+A53vo0r66xzB6E82inKG2J7qtwdi+Kkw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.3.1.tgz", + "integrity": "sha512-vkMXHQ0BcLFysBMWgSBLSk3+leMpFSyyFj8zQtv5ZyUBx8/owVh1/pPEkzmW/DR/Gy/5c8vjLDD9gZjXNKbrpA==", "dependencies": { "@smithy/config-resolver": "^2.2.0", "@smithy/credential-provider-imds": "^2.3.0", "@smithy/node-config-provider": "^2.3.0", "@smithy/property-provider": "^2.2.0", - "@smithy/smithy-client": "^2.5.0", + "@smithy/smithy-client": "^2.5.1", "@smithy/types": "^2.12.0", "tslib": "^2.6.2" }, @@ -4880,12 +4915,6 @@ "pretty-format": "^29.0.0" } }, - "node_modules/@types/jmespath": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@types/jmespath/-/jmespath-0.15.2.tgz", - "integrity": "sha512-pegh49FtNsC389Flyo9y8AfkVIZn9MMPE9yJrO9svhq6Fks2MwymULWjZqySuxmctd3ZH4/n7Mr98D+1Qo5vGA==", - "dev": true - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4926,9 +4955,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.6.tgz", - "integrity": "sha512-3KurE8taB8GCvZBPngVbp0lk5CKi8M9f9k1rsADh0Evdz5SzJ+Q+Hx9uHoFGsLnLnd1xmkDQr2hVhlA0Mn0lKQ==", + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", "dependencies": { "undici-types": "~5.26.4" } @@ -5632,9 +5661,9 @@ } }, "node_modules/aws-cdk": { - "version": "2.136.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.136.0.tgz", - "integrity": "sha512-MVSE+AERoP0D1qXlkhKQOzs22QVulGleX1yJTkWzoYhEyseEmR8EiFJcmyEhJku/swmY0KDpVlT9R62dRG5+JQ==", + "version": "2.137.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.137.0.tgz", + "integrity": "sha512-3pf3SVDwNZvo3EfhO3yl1B+KbRHz7T4UmPifUEKfOwk7ABAFLRSNddZuUlF560XSBTFLkrZoeBDa0/MLJT6F4g==", "bin": { "cdk": "bin/cdk" }, @@ -5646,9 +5675,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.136.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.136.0.tgz", - "integrity": "sha512-zdkWNe91mvZH6ESghUoIxB8ORoreExg2wowTLEVfy3vWY1a6n69crxk8mkCG+vn6GhXEnEPpovoG1QV8BpXTpA==", + "version": "2.137.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.137.0.tgz", + "integrity": "sha512-pD3AGdKBa8q1+vVWRabiDHuecVMlP8ERGPHc9Pb0dVlpbC/ODC6XXC1S0TAMsr0JI5Lh6pk4vL5cC+spsMeotw==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -6007,9 +6036,9 @@ } }, "node_modules/aws-sdk": { - "version": "2.1595.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1595.0.tgz", - "integrity": "sha512-ee0FaplSMz9Y6XJnnyDCHv6SLziJ2YCI4SsO0VRFUKK4Jtk/KErp20CJI/4ZsS+oz7k2/vQ3JqGQXCz95nU8Ww==", + "version": "2.1599.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1599.0.tgz", + "integrity": "sha512-jPb1LAN+s1TLTK+VR3TTJLr//sb3AhhT60Bm9jxB5G/fVeeRczXtBtixNpQ00gksQdkstILYLc9S6MuKMsksxA==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -7419,6 +7448,15 @@ "node": ">=12" } }, + "node_modules/drange": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", + "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -10610,6 +10648,7 @@ "version": "0.16.0", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "dev": true, "engines": { "node": ">= 0.6.0" } @@ -14191,6 +14230,19 @@ "node": ">=8" } }, + "node_modules/randexp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", + "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==", + "dev": true, + "dependencies": { + "drange": "^1.0.2", + "ret": "^0.2.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -14668,6 +14720,15 @@ "node": ">=8" } }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -16676,9 +16737,9 @@ } }, "node_modules/typescript": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz", - "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17242,9 +17303,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/batch": { "name": "@aws-lambda-powertools/batch", - "version": "2.0.4", + "version": "2.1.0", "license": "MIT-0", "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing" @@ -17252,7 +17321,7 @@ }, "packages/commons": { "name": "@aws-lambda-powertools/commons", - "version": "2.0.4", + "version": "2.1.0", "license": "MIT-0", "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing" @@ -17260,17 +17329,16 @@ }, "packages/idempotency": { "name": "@aws-lambda-powertools/idempotency", - "version": "2.0.4", + "version": "2.1.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.0.4", - "jmespath": "^0.16.0" + "@aws-lambda-powertools/commons": "^2.1.0", + "@aws-lambda-powertools/jmespath": "^2.1.0" }, "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-dynamodb": "^3.549.0", - "@aws-sdk/lib-dynamodb": "^3.549.0", - "@types/jmespath": "^0.15.0", + "@aws-sdk/client-dynamodb": "^3.554.0", + "@aws-sdk/lib-dynamodb": "^3.554.0", "aws-sdk-client-mock": "^4.0.0", "aws-sdk-client-mock-jest": "^4.0.0" }, @@ -17293,18 +17361,18 @@ }, "packages/jmespath": { "name": "@aws-lambda-powertools/jmespath", - "version": "2.0.4", + "version": "2.1.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.0.4" + "@aws-lambda-powertools/commons": "^2.1.0" } }, "packages/logger": { "name": "@aws-lambda-powertools/logger", - "version": "2.0.4", + "version": "2.1.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.0.4", + "@aws-lambda-powertools/commons": "^2.1.0", "lodash.merge": "^4.6.2" }, "devDependencies": { @@ -17322,14 +17390,14 @@ }, "packages/metrics": { "name": "@aws-lambda-powertools/metrics", - "version": "2.0.4", + "version": "2.1.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.0.4" + "@aws-lambda-powertools/commons": "^2.1.0" }, "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-cloudwatch": "^3.549.0", + "@aws-sdk/client-cloudwatch": "^3.554.0", "@types/promise-retry": "^1.1.3", "promise-retry": "^2.0.1" }, @@ -17344,18 +17412,18 @@ }, "packages/parameters": { "name": "@aws-lambda-powertools/parameters", - "version": "2.0.4", + "version": "2.1.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.0.4" + "@aws-lambda-powertools/commons": "^2.1.0" }, "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-appconfigdata": "^3.549.0", - "@aws-sdk/client-dynamodb": "^3.549.0", - "@aws-sdk/client-secrets-manager": "^3.549.0", - "@aws-sdk/client-ssm": "^3.549.0", - "@aws-sdk/util-dynamodb": "^3.549.0", + "@aws-sdk/client-appconfigdata": "^3.554.0", + "@aws-sdk/client-dynamodb": "^3.554.0", + "@aws-sdk/client-secrets-manager": "^3.554.0", + "@aws-sdk/client-ssm": "^3.554.0", + "@aws-sdk/util-dynamodb": "^3.554.0", "@smithy/util-base64": "^2.3.0", "aws-sdk-client-mock": "^4.0.0", "aws-sdk-client-mock-jest": "^4.0.0" @@ -17389,32 +17457,44 @@ } } }, + "packages/parser": { + "name": "@aws-lambda-powertools/parser", + "version": "2.1.0", + "license": "MIT-0", + "devDependencies": { + "@anatine/zod-mock": "^3.13.3", + "@faker-js/faker": "^8.3.1" + }, + "peerDependencies": { + "zod": ">=3.x" + } + }, "packages/testing": { "name": "@aws-lambda-powertools/testing-utils", - "version": "2.0.4", + "version": "2.1.0", "license": "MIT-0", "dependencies": { "@aws-cdk/cli-lib-alpha": "^2.121.1-alpha.0", - "@aws-sdk/client-lambda": "^3.549.0", + "@aws-sdk/client-lambda": "^3.554.0", "@smithy/util-utf8": "^2.3.0", - "aws-cdk-lib": "^2.136.0", + "aws-cdk-lib": "^2.137.0", "esbuild": "^0.20.2" } }, "packages/tracer": { "name": "@aws-lambda-powertools/tracer", - "version": "2.0.4", + "version": "2.1.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^2.0.4", + "@aws-lambda-powertools/commons": "^2.1.0", "aws-xray-sdk-core": "^3.6.0" }, "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-dynamodb": "^3.549.0", - "@aws-sdk/client-xray": "^3.549.0", + "@aws-sdk/client-dynamodb": "^3.554.0", + "@aws-sdk/client-xray": "^3.554.0", "@types/promise-retry": "^1.1.6", - "aws-sdk": "^2.1595.0", + "aws-sdk": "^2.1599.0", "axios": "^1.6.8", "promise-retry": "^2.0.1" }, diff --git a/package.json b/package.json index 726cb3146c..ea87efdd6d 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "A suite of utilities for AWS Lambda functions to ease adopting best practices such as tracing, structured logging, custom metrics, and more.", "workspaces": [ "packages/commons", + "packages/jmespath", "packages/logger", "packages/metrics", "packages/tracer", @@ -11,7 +12,7 @@ "packages/idempotency", "packages/batch", "packages/testing", - "packages/jmespath", + "packages/parser", "docs/snippets", "layers", "examples/app" @@ -50,7 +51,7 @@ "@middy/core": "^4.7.0", "@types/aws-lambda": "^8.10.137", "@types/jest": "^29.5.12", - "@types/node": "^20.12.6", + "@types/node": "^20.12.7", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "eslint": "^8.57.0", @@ -69,7 +70,7 @@ "ts-node": "^10.9.2", "typedoc": "^0.25.13", "typedoc-plugin-missing-exports": "^2.2.0", - "typescript": "^5.4.4" + "typescript": "^5.4.5" }, "engines": { "node": ">=16" diff --git a/packages/batch/CHANGELOG.md b/packages/batch/CHANGELOG.md index 984ce8950a..b06eb267f6 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.1.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.0.4...v2.1.0) (2024-04-17) + +**Note:** Version bump only for package @aws-lambda-powertools/batch + + + + + ## 2.0.4 (2024-04-10) **Note:** Version bump only for package @aws-lambda-powertools/batch diff --git a/packages/batch/package.json b/packages/batch/package.json index a820215dac..1dea56f509 100644 --- a/packages/batch/package.json +++ b/packages/batch/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/batch", - "version": "2.0.4", + "version": "2.1.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 1acd8487e3..8639ebc249 100644 --- a/packages/commons/CHANGELOG.md +++ b/packages/commons/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.1.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.0.4...v2.1.0) (2024-04-17) + +**Note:** Version bump only for package @aws-lambda-powertools/commons + + + + + ## 2.0.4 (2024-04-10) **Note:** Version bump only for package @aws-lambda-powertools/commons diff --git a/packages/commons/package.json b/packages/commons/package.json index 6b8da1bf1e..bbe5ab032f 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/commons", - "version": "2.0.4", + "version": "2.1.0", "description": "A shared utility package for Powertools for AWS Lambda (TypeScript) libraries", "author": { "name": "Amazon Web Services", diff --git a/packages/commons/src/version.ts b/packages/commons/src/version.ts index 5ad397ea9a..504dfc4fc1 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.0.4'; +export const PT_VERSION = '2.1.0'; diff --git a/packages/idempotency/CHANGELOG.md b/packages/idempotency/CHANGELOG.md index 31c37086af..aa264f4aa1 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.1.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.0.4...v2.1.0) (2024-04-17) + + +### Features + +* **idempotency:** add custom JMESPath functions ([#2364](https://github.com/aws-powertools/powertools-lambda-typescript/issues/2364)) ([9721e7c](https://github.com/aws-powertools/powertools-lambda-typescript/commit/9721e7c01fc010944eb477bdbc24b9e06a5c4571)) + + + + + ## 2.0.4 (2024-04-10) ### Bug Fixes diff --git a/packages/idempotency/README.md b/packages/idempotency/README.md index 6d8edd7061..b811e5be09 100644 --- a/packages/idempotency/README.md +++ b/packages/idempotency/README.md @@ -18,7 +18,6 @@ You can use the package in both TypeScript and JavaScript code bases. - [Becoming a reference customer](#becoming-a-reference-customer) - [Sharing your work](#sharing-your-work) - [Using Lambda Layer](#using-lambda-layer) -- [Credits](#credits) - [License](#license) ## Intro @@ -158,7 +157,33 @@ export const handler = makeIdempotent(myHandler, { config: new IdempotencyConfig({ eventKeyJmespath: 'requestContext.identity.user', }), -}); +}); +``` + +Additionally, you can also use one of the [JMESPath built-in functions](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/jmespath/#built-in-jmespath-functions) like `powertools_json()` to decode keys and use parts of the payload as the idempotency key. + +```ts +import { makeIdempotent, IdempotencyConfig } from '@aws-lambda-powertools/idempotency'; +import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb'; +import type { Context, APIGatewayProxyEvent } from 'aws-lambda'; + +const persistenceStore = new DynamoDBPersistenceLayer({ + tableName: 'idempotencyTableName', +}); + +const myHandler = async ( + event: APIGatewayProxyEvent, + _context: Context +): Promise => { + // your code goes here here +}; + +export const handler = makeIdempotent(myHandler, { + persistenceStore, + config: new IdempotencyConfig({ + eventKeyJmespath: 'powertools_json(body).["user", "productId"]', + }), +}); ``` Check the [docs](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/idempotency/) for more examples. @@ -311,12 +336,8 @@ Share what you did with Powertools for AWS Lambda (TypeScript) 💞💞. Blog po ### Using Lambda Layer -This helps us understand who uses Powertools for AWS Lambda (TypeScript) in a non-intrusive way, and helps us gain future investments for other Powertools for AWS Lambda languages. When [using Layers](#lambda-layers), you can add Powertools as a dev dependency (or as part of your virtual env) to not impact the development process. - -## Credits - -Credits for the Lambda Powertools for AWS Lambda (TypeScript) idea go to [DAZN](https://github.com/getndazn) and their [DAZN Lambda Powertools](https://github.com/getndazn/dazn-lambda-powertools/). +This helps us understand who uses Powertools for AWS Lambda (TypeScript) in a non-intrusive way, and helps us gain future investments for other Powertools for AWS Lambda languages. When [using Layers](https://docs.powertools.aws.dev/lambda/typescript/latest/#lambda-layer), you can add Powertools as a dev dependency to not impact the development process. ## License -This library is licensed under the MIT-0 License. See the LICENSE file. +This library is licensed under the MIT-0 License. See the LICENSE file. \ No newline at end of file diff --git a/packages/idempotency/package.json b/packages/idempotency/package.json index 33aa029bbd..fd2dd2ab4c 100644 --- a/packages/idempotency/package.json +++ b/packages/idempotency/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/idempotency", - "version": "2.0.4", + "version": "2.1.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", @@ -100,8 +100,8 @@ "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" }, "dependencies": { - "@aws-lambda-powertools/commons": "^2.0.4", - "jmespath": "^0.16.0" + "@aws-lambda-powertools/commons": "^2.1.0", + "@aws-lambda-powertools/jmespath": "^2.1.0" }, "peerDependencies": { "@aws-sdk/client-dynamodb": ">=3.x", @@ -129,9 +129,8 @@ ], "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-dynamodb": "^3.549.0", - "@aws-sdk/lib-dynamodb": "^3.549.0", - "@types/jmespath": "^0.15.0", + "@aws-sdk/client-dynamodb": "^3.554.0", + "@aws-sdk/lib-dynamodb": "^3.554.0", "aws-sdk-client-mock": "^4.0.0", "aws-sdk-client-mock-jest": "^4.0.0" } diff --git a/packages/idempotency/src/IdempotencyConfig.ts b/packages/idempotency/src/IdempotencyConfig.ts index 0f5afc6f09..0a89a4af4c 100644 --- a/packages/idempotency/src/IdempotencyConfig.ts +++ b/packages/idempotency/src/IdempotencyConfig.ts @@ -1,6 +1,8 @@ import { EnvironmentVariablesService } from './config/EnvironmentVariablesService.js'; import type { Context } from 'aws-lambda'; import type { IdempotencyConfigOptions } from './types/IdempotencyOptions.js'; +import type { JMESPathParsingOptions } from '@aws-lambda-powertools/jmespath/types'; +import { PowertoolsFunctions } from '@aws-lambda-powertools/jmespath/functions'; /** * Configuration for the idempotency feature. @@ -22,6 +24,14 @@ class IdempotencyConfig { * @default 'md5' */ public hashFunction: string; + /** + * Options for parsing JMESPath expressions. + * + * By default, you can use any of the {@link https://jmespath.org/specification.html | JMESPath built-in functions} as well as the + * {@link https://docs.powertools.aws.dev/lambda/typescript/latest/api/classes/_aws_lambda_powertools_jmespath.PowertoolsFunctions.PowertoolsFunctions.html | custom functions provided} + * by the `@aws-lambda-powertools/jmespath` package. + */ + public jmesPathOptions: JMESPathParsingOptions; /** * The lambda context object. */ @@ -53,6 +63,7 @@ class IdempotencyConfig { public constructor(config: IdempotencyConfigOptions) { this.eventKeyJmesPath = config.eventKeyJmesPath ?? ''; this.payloadValidationJmesPath = config.payloadValidationJmesPath; + this.jmesPathOptions = { customFunctions: new PowertoolsFunctions() }; this.throwOnNoIdempotencyKey = config.throwOnNoIdempotencyKey ?? false; this.expiresAfterSeconds = config.expiresAfterSeconds ?? 3600; // 1 hour default this.useLocalCache = config.useLocalCache ?? false; diff --git a/packages/idempotency/src/IdempotencyHandler.ts b/packages/idempotency/src/IdempotencyHandler.ts index 03f06b1c2d..7954020bf3 100644 --- a/packages/idempotency/src/IdempotencyHandler.ts +++ b/packages/idempotency/src/IdempotencyHandler.ts @@ -16,7 +16,7 @@ import { BasePersistenceLayer } from './persistence/BasePersistenceLayer.js'; import { IdempotencyRecord } from './persistence/IdempotencyRecord.js'; import { IdempotencyConfig } from './IdempotencyConfig.js'; import { MAX_RETRIES, IdempotencyRecordStatus } from './constants.js'; -import { search } from 'jmespath'; +import { search } from '@aws-lambda-powertools/jmespath'; /** * @internal @@ -275,8 +275,9 @@ export class IdempotencyHandler { !this.#idempotencyConfig.throwOnNoIdempotencyKey ) { const selection = search( + this.#idempotencyConfig.eventKeyJmesPath, this.#functionPayloadToBeHashed, - this.#idempotencyConfig.eventKeyJmesPath + this.#idempotencyConfig.jmesPathOptions ); return selection === undefined || selection === null; diff --git a/packages/idempotency/src/persistence/BasePersistenceLayer.ts b/packages/idempotency/src/persistence/BasePersistenceLayer.ts index df9ed41a9e..1456d70f12 100644 --- a/packages/idempotency/src/persistence/BasePersistenceLayer.ts +++ b/packages/idempotency/src/persistence/BasePersistenceLayer.ts @@ -1,5 +1,6 @@ import { createHash, Hash } from 'node:crypto'; -import { search } from 'jmespath'; +import { search } from '@aws-lambda-powertools/jmespath'; +import type { JMESPathParsingOptions } from '@aws-lambda-powertools/jmespath/types'; import type { BasePersistenceLayerOptions, BasePersistenceLayerInterface, @@ -36,6 +37,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { private throwOnNoIdempotencyKey = false; private useLocalCache = false; private validationKeyJmesPath?: string; + #jmesPathOptions?: JMESPathParsingOptions; public constructor() { this.envVarsService = new EnvironmentVariablesService(); @@ -63,6 +65,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { this.eventKeyJmesPath = idempotencyConfig?.eventKeyJmesPath; this.validationKeyJmesPath = idempotencyConfig?.payloadValidationJmesPath; + this.#jmesPathOptions = idempotencyConfig.jmesPathOptions; this.payloadValidationEnabled = this.validationKeyJmesPath !== undefined || false; this.throwOnNoIdempotencyKey = @@ -279,7 +282,11 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { */ private getHashedIdempotencyKey(data: JSONValue): string { if (this.eventKeyJmesPath) { - data = search(data, this.eventKeyJmesPath); + data = search( + this.eventKeyJmesPath, + data, + this.#jmesPathOptions + ) as JSONValue; } if (BasePersistenceLayer.isMissingIdempotencyKey(data)) { @@ -305,7 +312,11 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { */ private getHashedPayload(data: JSONValue): string { if (this.isPayloadValidationEnabled() && this.validationKeyJmesPath) { - data = search(data, this.validationKeyJmesPath); + data = search( + this.validationKeyJmesPath, + data, + this.#jmesPathOptions + ) as JSONValue; return this.generateHash(JSON.stringify(data)); } else { diff --git a/packages/idempotency/tests/e2e/idempotentDecorator.test.ts b/packages/idempotency/tests/e2e/idempotentDecorator.test.ts index 83a863afaa..40146a8f8c 100644 --- a/packages/idempotency/tests/e2e/idempotentDecorator.test.ts +++ b/packages/idempotency/tests/e2e/idempotentDecorator.test.ts @@ -63,6 +63,7 @@ describe('Idempotency e2e test decorator, default settings', () => { }, { nameSuffix: 'defaultParallel', + outputFormat: 'ESM', } ); @@ -79,6 +80,7 @@ describe('Idempotency e2e test decorator, default settings', () => { }, { nameSuffix: 'timeout', + outputFormat: 'ESM', } ); @@ -95,6 +97,7 @@ describe('Idempotency e2e test decorator, default settings', () => { }, { nameSuffix: 'expired', + outputFormat: 'ESM', } ); @@ -110,6 +113,7 @@ describe('Idempotency e2e test decorator, default settings', () => { }, { nameSuffix: 'dataIndex', + outputFormat: 'ESM', } ); @@ -131,6 +135,7 @@ describe('Idempotency e2e test decorator, default settings', () => { }, { nameSuffix: 'customConfig', + outputFormat: 'ESM', } ); diff --git a/packages/idempotency/tests/e2e/makeIdempotent.test.FunctionCode.ts b/packages/idempotency/tests/e2e/makeIdempotent.test.FunctionCode.ts index 26d045a6fd..2fd70bf6a1 100644 --- a/packages/idempotency/tests/e2e/makeIdempotent.test.FunctionCode.ts +++ b/packages/idempotency/tests/e2e/makeIdempotent.test.FunctionCode.ts @@ -90,16 +90,17 @@ export const handlerCustomized = async ( * Test idempotent Lambda handler with JMESPath expression to extract event key. */ export const handlerLambda = makeIdempotent( - async (event: { foo: string }, context: Context) => { + async (event: { body: string }, context: Context) => { logger.addContext(context); - logger.info(`foo`, { details: event.foo }); + const body = JSON.parse(event.body); + logger.info('foo', { details: body.foo }); - return event.foo; + return body.foo; }, { persistenceStore: dynamoDBPersistenceLayer, config: new IdempotencyConfig({ - eventKeyJmesPath: 'foo', + eventKeyJmesPath: 'powertools_json(body).foo', useLocalCache: true, }), } diff --git a/packages/idempotency/tests/e2e/makeIdempotent.test.ts b/packages/idempotency/tests/e2e/makeIdempotent.test.ts index 63da32a705..f9d5e5bf6f 100644 --- a/packages/idempotency/tests/e2e/makeIdempotent.test.ts +++ b/packages/idempotency/tests/e2e/makeIdempotent.test.ts @@ -268,10 +268,12 @@ describe(`Idempotency E2E tests, wrapper function usage`, () => { async () => { // Prepare const payload = { - foo: 'bar', + body: JSON.stringify({ + foo: 'bar', + }), }; const payloadHash = createHash('md5') - .update(JSON.stringify(payload.foo)) + .update(JSON.stringify('bar')) .digest('base64'); // Act diff --git a/packages/jmespath/CHANGELOG.md b/packages/jmespath/CHANGELOG.md index d777671716..ee58a16690 100644 --- a/packages/jmespath/CHANGELOG.md +++ b/packages/jmespath/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.1.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.0.4...v2.1.0) (2024-04-17) + + +### Bug Fixes + +* **jmespath:** refactor custom function introspection to work with minification ([#2384](https://github.com/aws-powertools/powertools-lambda-typescript/issues/2384)) ([21ecc4f](https://github.com/aws-powertools/powertools-lambda-typescript/commit/21ecc4f736ccba85c276889163860a98613174cc)) + + + + + ## 2.0.4 (2024-04-10) ### Features diff --git a/packages/jmespath/package.json b/packages/jmespath/package.json index 04bd13dca7..8f02821021 100644 --- a/packages/jmespath/package.json +++ b/packages/jmespath/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/jmespath", - "version": "2.0.4", + "version": "2.1.0", "description": "A type safe and modern jmespath module to parse and extract data from JSON documents using JMESPath", "author": { "name": "Amazon Web Services", @@ -74,7 +74,7 @@ "lib" ], "dependencies": { - "@aws-lambda-powertools/commons": "^2.0.4" + "@aws-lambda-powertools/commons": "^2.1.0" }, "repository": { "type": "git", diff --git a/packages/jmespath/src/Functions.ts b/packages/jmespath/src/Functions.ts index e16e0a4e8f..972c649849 100644 --- a/packages/jmespath/src/Functions.ts +++ b/packages/jmespath/src/Functions.ts @@ -48,7 +48,11 @@ import { arityCheck, typeCheck } from './utils.js'; * ``` */ class Functions { + /** + * A set of all the custom functions available in this and all child classes. + */ public methods: Set = new Set(); + /** * Get the absolute value of the provided number. * @@ -528,14 +532,32 @@ class Functions { return Object.values(arg); } + /** + * Lazily introspects the methods of the class instance and all child classes + * to get the names of the methods that correspond to JMESPath functions. + * + * This method is used to get the names of the custom functions that are available + * in the class instance and all child classes. The names of the functions are used + * to create the custom function map that is passed to the JMESPath search function. + * + * The method traverses the inheritance chain going from the leaf class to the root class + * and stops when it reaches the `Functions` class, which is the root class. + * + * In doing so, it collects the names of the methods that start with `func` and adds them + * to the `methods` set. Finally, when the recursion collects back to the current instance, + * it adds the collected methods to the `this.methods` set so that they can be accessed later. + * + * @param scope The scope of the class instance to introspect + */ public introspectMethods(scope?: Functions): Set { const prototype = Object.getPrototypeOf(this); - const ownName = prototype.constructor.name; const methods = new Set(); - if (ownName !== 'Functions') { + if (this instanceof Functions) { for (const method of prototype.introspectMethods(scope)) { methods.add(method); } + } else { + return methods; } // This block is executed for every class in the inheritance chain diff --git a/packages/jmespath/src/ParsedResult.ts b/packages/jmespath/src/ParsedResult.ts index 63610bff31..4d6733dbcc 100644 --- a/packages/jmespath/src/ParsedResult.ts +++ b/packages/jmespath/src/ParsedResult.ts @@ -5,7 +5,7 @@ import { UnknownFunctionError, VariadicArityError, } from './errors.js'; -import type { Node, ParsingOptions, JSONObject } from './types.js'; +import type { Node, JMESPathParsingOptions, JSONObject } from './types.js'; class ParsedResult { public expression: string; @@ -22,7 +22,7 @@ class ParsedResult { * @param value The JSON value to search * @param options The parsing options to use */ - public search(value: JSONObject, options?: ParsingOptions): unknown { + public search(value: JSONObject, options?: JMESPathParsingOptions): unknown { const interpreter = new TreeInterpreter(options); try { diff --git a/packages/jmespath/src/TreeInterpreter.ts b/packages/jmespath/src/TreeInterpreter.ts index 28d2c531bc..ee2292bd99 100644 --- a/packages/jmespath/src/TreeInterpreter.ts +++ b/packages/jmespath/src/TreeInterpreter.ts @@ -12,7 +12,7 @@ import { } from './errors.js'; import { Expression } from './Expression.js'; import { Functions } from './Functions.js'; -import type { Node, TreeInterpreterOptions, JSONObject } from './types.js'; +import type { Node, JMESPathParsingOptions, JSONObject } from './types.js'; import { isTruthy, sliceArray } from './utils.js'; /** @@ -30,7 +30,7 @@ class TreeInterpreter { /** * @param options The options to use for the interpreter. */ - public constructor(options?: TreeInterpreterOptions) { + public constructor(options?: JMESPathParsingOptions) { if (options?.customFunctions) { this.#functions = options.customFunctions; } else { diff --git a/packages/jmespath/src/envelopes.ts b/packages/jmespath/src/envelopes.ts index 84cf06c383..2fa2607c85 100644 --- a/packages/jmespath/src/envelopes.ts +++ b/packages/jmespath/src/envelopes.ts @@ -1,6 +1,6 @@ import { search } from './search.js'; import { PowertoolsFunctions } from './PowertoolsFunctions.js'; -import type { ParsingOptions, JSONObject } from './types.js'; +import type { JMESPathParsingOptions, JSONObject } from './types.js'; /** * Searches and extracts data using JMESPath @@ -57,7 +57,7 @@ import type { ParsingOptions, JSONObject } from './types.js'; const extractDataFromEnvelope = ( data: JSONObject, envelope: string, - options?: ParsingOptions + options?: JMESPathParsingOptions ): T => { if (!options) { options = { customFunctions: new PowertoolsFunctions() }; diff --git a/packages/jmespath/src/search.ts b/packages/jmespath/src/search.ts index 8f53775fa5..44afcd61c4 100644 --- a/packages/jmespath/src/search.ts +++ b/packages/jmespath/src/search.ts @@ -1,5 +1,5 @@ import { Parser } from './Parser.js'; -import type { ParsingOptions, JSONObject } from './types.js'; +import type { JMESPathParsingOptions, JSONObject } from './types.js'; const parser = new Parser(); @@ -52,7 +52,7 @@ const parser = new Parser(); const search = ( expression: string, data: JSONObject, - options?: ParsingOptions + options?: JMESPathParsingOptions ): unknown => { return parser.parse(expression).search(data, options); }; diff --git a/packages/jmespath/src/types.ts b/packages/jmespath/src/types.ts index 07eef2fb33..19d102e6b3 100644 --- a/packages/jmespath/src/types.ts +++ b/packages/jmespath/src/types.ts @@ -24,19 +24,6 @@ type Node = { value?: JSONValue; }; -/** - * Options for the tree interpreter. - */ -type TreeInterpreterOptions = { - /** - * The custom functions to use. - * - * By default, the interpreter uses the standard JMESPath functions - * available in the [JMESPath specification](https://jmespath.org/specification.html). - */ - customFunctions?: Functions; -}; - /** * Options for parsing. * @@ -57,7 +44,15 @@ type TreeInterpreterOptions = { * console.log(result); // { a: 1 } * ``` */ -type ParsingOptions = TreeInterpreterOptions; +type JMESPathParsingOptions = { + /** + * The custom functions to use. + * + * By default, the interpreter uses the standard JMESPath functions + * available in the [JMESPath specification](https://jmespath.org/specification.html). + */ + customFunctions?: Functions; +}; /** * Decorator for function signatures. @@ -103,8 +98,7 @@ export type { FunctionSignatureDecorator, FunctionSignatureOptions, Node, - ParsingOptions, + JMESPathParsingOptions, Token, - TreeInterpreterOptions, JSONObject, }; diff --git a/packages/jmespath/tests/unit/index.test.ts b/packages/jmespath/tests/unit/index.test.ts index a06f3328d2..a8cb33201c 100644 --- a/packages/jmespath/tests/unit/index.test.ts +++ b/packages/jmespath/tests/unit/index.test.ts @@ -351,9 +351,6 @@ describe('Coverage tests', () => { it('uses the custom function extending the powertools custom functions', () => { // Prepare class CustomFunctions extends PowertoolsFunctions { - public constructor() { - super(); - } @PowertoolsFunctions.signature({ argumentsSpecs: [['string']], }) @@ -384,5 +381,26 @@ describe('Coverage tests', () => { // Assess expect(messages).toStrictEqual(['hello world']); }); + + it('correctly registers all the custom functions', () => { + // Prepare + class CustomFunctions extends PowertoolsFunctions { + @PowertoolsFunctions.signature({ + argumentsSpecs: [['string']], + }) + public funcPassThrough(value: string): string { + return value; + } + } + + // Act + const customFunctions = new CustomFunctions(); + search('pass_through(foo)', { foo: 'bar' }, { customFunctions }); + + // Assess + expect(customFunctions.methods.size).toBeGreaterThan( + new PowertoolsFunctions().methods.size + ); + }); }); }); diff --git a/packages/logger/CHANGELOG.md b/packages/logger/CHANGELOG.md index 8ed39bb05e..7ad2b20095 100644 --- a/packages/logger/CHANGELOG.md +++ b/packages/logger/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.1.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.0.4...v2.1.0) (2024-04-17) + +**Note:** Version bump only for package @aws-lambda-powertools/logger + + + + + ## 2.0.4 (2024-04-10) ### Bug Fixes diff --git a/packages/logger/package.json b/packages/logger/package.json index dd2e4c6a04..7499ee4787 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/logger", - "version": "2.0.4", + "version": "2.1.0", "description": "The logging package for the Powertools for AWS Lambda (TypeScript) library", "author": { "name": "Amazon Web Services", @@ -88,7 +88,7 @@ "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" }, "dependencies": { - "@aws-lambda-powertools/commons": "^2.0.4", + "@aws-lambda-powertools/commons": "^2.1.0", "lodash.merge": "^4.6.2" }, "keywords": [ diff --git a/packages/logger/tests/e2e/basicFeatures.middy.test.ts b/packages/logger/tests/e2e/basicFeatures.middy.test.ts index 5f7a67eb35..efdb9668ed 100644 --- a/packages/logger/tests/e2e/basicFeatures.middy.test.ts +++ b/packages/logger/tests/e2e/basicFeatures.middy.test.ts @@ -48,6 +48,7 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { { logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, nameSuffix: 'BasicFeatures', + outputFormat: 'ESM', } ); diff --git a/packages/logger/tests/e2e/sampleRate.decorator.test.ts b/packages/logger/tests/e2e/sampleRate.decorator.test.ts index d6db70c8c0..ff4b5fae30 100644 --- a/packages/logger/tests/e2e/sampleRate.decorator.test.ts +++ b/packages/logger/tests/e2e/sampleRate.decorator.test.ts @@ -52,6 +52,7 @@ describe(`Logger E2E tests, sample rate and injectLambdaContext()`, () => { { logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, nameSuffix: 'BasicFeatures', + outputFormat: 'ESM', } ); diff --git a/packages/metrics/CHANGELOG.md b/packages/metrics/CHANGELOG.md index 6470f76dd4..840853630c 100644 --- a/packages/metrics/CHANGELOG.md +++ b/packages/metrics/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.1.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.0.4...v2.1.0) (2024-04-17) + +**Note:** Version bump only for package @aws-lambda-powertools/metrics + + + + + ## 2.0.4 (2024-04-10) **Note:** Version bump only for package @aws-lambda-powertools/metrics diff --git a/packages/metrics/package.json b/packages/metrics/package.json index 931e43f192..bdfc48b56b 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/metrics", - "version": "2.0.4", + "version": "2.1.0", "description": "The metrics package for the Powertools for AWS Lambda (TypeScript) library", "author": { "name": "Amazon Web Services", @@ -66,7 +66,7 @@ "main": "./lib/cjs/index.js", "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-cloudwatch": "^3.549.0", + "@aws-sdk/client-cloudwatch": "^3.554.0", "@types/promise-retry": "^1.1.3", "promise-retry": "^2.0.1" }, @@ -89,7 +89,7 @@ "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" }, "dependencies": { - "@aws-lambda-powertools/commons": "^2.0.4" + "@aws-lambda-powertools/commons": "^2.1.0" }, "keywords": [ "aws", diff --git a/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts b/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts index 5bf1a38809..7ad7258916 100644 --- a/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts +++ b/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts @@ -50,6 +50,7 @@ describe(`Metrics E2E tests, basic features decorator usage`, () => { }, { nameSuffix: 'BasicFeatures', + outputFormat: 'ESM', } ); diff --git a/packages/parameters/CHANGELOG.md b/packages/parameters/CHANGELOG.md index 62d972937a..ae100b877c 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.1.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.0.4...v2.1.0) (2024-04-17) + +**Note:** Version bump only for package @aws-lambda-powertools/parameters + + + + + ## 2.0.4 (2024-04-10) **Note:** Version bump only for package @aws-lambda-powertools/parameters diff --git a/packages/parameters/package.json b/packages/parameters/package.json index 764d802216..e515c8c07c 100644 --- a/packages/parameters/package.json +++ b/packages/parameters/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/parameters", - "version": "2.0.4", + "version": "2.1.0", "description": "The parameters package for the Powertools for AWS Lambda (TypeScript) library", "author": { "name": "Amazon Web Services", @@ -158,17 +158,17 @@ ], "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-appconfigdata": "^3.549.0", - "@aws-sdk/client-dynamodb": "^3.549.0", - "@aws-sdk/client-secrets-manager": "^3.549.0", - "@aws-sdk/client-ssm": "^3.549.0", - "@aws-sdk/util-dynamodb": "^3.549.0", + "@aws-sdk/client-appconfigdata": "^3.554.0", + "@aws-sdk/client-dynamodb": "^3.554.0", + "@aws-sdk/client-secrets-manager": "^3.554.0", + "@aws-sdk/client-ssm": "^3.554.0", + "@aws-sdk/util-dynamodb": "^3.554.0", "@smithy/util-base64": "^2.3.0", "aws-sdk-client-mock": "^4.0.0", "aws-sdk-client-mock-jest": "^4.0.0" }, "dependencies": { - "@aws-lambda-powertools/commons": "^2.0.4" + "@aws-lambda-powertools/commons": "^2.1.0" }, "peerDependencies": { "@aws-sdk/client-appconfigdata": ">=3.x", diff --git a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts index cfbf7cd58d..c44d29c405 100644 --- a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts +++ b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts @@ -120,6 +120,7 @@ describe(`Parameters E2E tests, AppConfig provider`, () => { }, { nameSuffix: 'appConfigProvider', + outputFormat: 'ESM', } ); diff --git a/packages/parameters/tests/e2e/secretsProvider.class.test.ts b/packages/parameters/tests/e2e/secretsProvider.class.test.ts index 9a8bffa9b7..32bcf1a5a0 100644 --- a/packages/parameters/tests/e2e/secretsProvider.class.test.ts +++ b/packages/parameters/tests/e2e/secretsProvider.class.test.ts @@ -60,6 +60,7 @@ describe(`Parameters E2E tests, Secrets Manager provider`, () => { }, { nameSuffix: 'secretsProvider', + outputFormat: 'ESM', } ); diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md new file mode 100644 index 0000000000..46ab4f0191 --- /dev/null +++ b/packages/parser/CHANGELOG.md @@ -0,0 +1,8 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [2.1.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.0.4...v2.1.0) (2024-04-17) + +**Note:** Version bump only for package @aws-lambda-powertools/parser diff --git a/packages/parser/README.md b/packages/parser/README.md new file mode 100644 index 0000000000..8b8c8e112b --- /dev/null +++ b/packages/parser/README.md @@ -0,0 +1,344 @@ +# Powertools for AWS Lambda (TypeScript) - Parser Utility + + +| ⚠️ **WARNING: Do not use this utility in production just yet!** ⚠️ | +| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| This AWS Lambda Powertools for TypeScript utility is currently released as beta developer preview and is intended strictly for feedback and testing purposes only.
This version is not stable, and significant breaking changes might incur before going [before the GA release](https://github.com/aws-powertools/powertools-lambda-typescript/milestone/16). | _ | + + + +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 package in both TypeScript and JavaScript code bases. + +- [Intro](#intro) +- [Key features](#key-features) +- [Usage](#usage) + - [Middleware](#middleware) + - [Decorator](#decorator) + - [Manual parsing](#manual-parsing) + - [Safe parsing](#safe-parsing) + - [Built-in schemas and envelopes](#built-in-schemas-and-envelopes) +- [Contribute](#contribute) +- [Roadmap](#roadmap) +- [Connect](#connect) +- [How to support Powertools for AWS Lambda (TypeScript)?](#how-to-support-powertools-for-aws-lambda-typescript) + - [Becoming a reference customer](#becoming-a-reference-customer) + - [Sharing your work](#sharing-your-work) + - [Using Lambda Layer](#using-lambda-layer) +- [Credits](#credits) +- [License](#license) + +## Intro + +The parser utility provides data validation and parsing using [Zod](https://zod.dev), a TypeScript-first schema declaration and validation library. + +## Key features + +* Define data schema as Zod schema, then parse, validate and extract only what you want +* Built-in envelopes to unwrap and validate popular AWS event sources payloads +* Extend and customize envelopes to fit your needs +* Safe parsing option to avoid throwing errors and custom error handling +* Available for Middy.js middleware and TypeScript method decorators + +## Usage + +To get started, install the library by running: + +```sh +npm install @aws-lambda-powertools/parser zod@~3 +``` + +Then, define your schema using Zod: + +```typescript +import { z } from 'zod'; + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +export { orderSchema }; +``` + +Next, you can parse incoming events using the `parser` decorator or Middy.js middleware: + +### Middleware + +```typescript +import type { Context } from 'aws-lambda'; +import { parser } from '@aws-lambda-powertools/parser/middleware'; +import { z } from 'zod'; +import middy from '@middy/core'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + 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 }) +); +``` + +### Decorator + +```typescript +import type { Context } from 'aws-lambda'; +import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { parser } from '@aws-lambda-powertools/parser'; +import { z } from 'zod'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +type Order = z.infer; + +class Lambda implements LambdaInterface { + @parser({ schema: orderSchema }) + public async handler(event: Order, _context: Context): Promise { + // event is now typed as Order + for (const item of event.items) { + logger.info('Processing item', { item }); + } + } +} + +const myFunction = new Lambda(); +export const handler = myFunction.handler.bind(myFunction); +``` + +### Manual parsing + +If you don't want to add an additional dependency, or you prefer the manual approach, you can `parse` the event directly by calling the `parse` method on schemas and envelopes: + +```typescript +import type { Context } from 'aws-lambda'; +import { z } from 'zod'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; +import { EventBridgeSchema } from '@aws-lambda-powertools/parser/schemas'; +import type { EventBridgeEvent } from '@aws-lambda-powertools/parser/types'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); +type Order = z.infer; + +export const handler = async ( + event: EventBridgeEvent, + _context: Context +): Promise => { + const parsedEvent = EventBridgeSchema.parse(event); + logger.info('Parsed event', parsedEvent); + + const orders: Order = EventBridgeEnvelope.parse(event, orderSchema); + logger.info('Parsed orders', orders); +}; +``` + +### Safe parsing + +When parsing data, you can use the `safeParse` method to avoid throwing errors and handle them manually: + + +```typescript +import type { Context } from 'aws-lambda'; +import { parser } from '@aws-lambda-powertools/parser/middleware'; +import { z } from 'zod'; +import middy from '@middy/core'; +import type { + ParsedResult, + EventBridgeEvent, +} from '@aws-lambda-powertools/parser/types'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + 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 }); + } + } else { + logger.error('Error parsing event', { event: event.error }); + logger.error('Original event', { event: event.originalEvent }); + } +}; + +export const handler = middy(lambdaHandler).use( + parser({ schema: orderSchema, safeParse: true }) +); +``` + +See the [safe parsing](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/parser#safe-parsing) section in the documentation for more details. + +### Built-in schemas and envelopes + +The utility provides a set of built-in schemas and envelopes to parse popular AWS event sources payloads, for example: + +```typescript +import type { Context } from 'aws-lambda'; +import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { parser } from '@aws-lambda-powertools/parser'; +import { z } from 'zod'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +type Order = z.infer; + +class Lambda implements LambdaInterface { + @parser({ schema: orderSchema, envelope: EventBridgeEnvelope }) + public async handler(event: Order, _context: Context): Promise { + // event is now typed as Order + for (const item of event.items) { + logger.info('Processing item', item); // (2)! + } + } +} + +const myFunction = new Lambda(); +export const handler = myFunction.handler.bind(myFunction); +``` + +Check the utility documentation for a complete list of built-in [schemas](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/parser/#built-in-schemas) and [envelopes](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/parser/#built-in-envelopes). + +## Contribute + +If you are interested in contributing to this project, please refer to our [Contributing Guidelines](https://github.com/aws-powertools/powertools-lambda-typescript/blob/main/CONTRIBUTING.md). + +## Roadmap + +The roadmap of Powertools for AWS Lambda (TypeScript) is driven by customers’ demand. +Help us prioritize upcoming functionalities or utilities by [upvoting existing RFCs and feature requests](https://github.com/aws-powertools/powertools-lambda-typescript/issues), or [creating new ones](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose), in this GitHub repository. + +## Connect + +* **Powertools for AWS Lambda on Discord**: `#typescript` - **[Invite link](https://discord.gg/B8zZKbbyET)** +* **Email**: aws-lambda-powertools-feedback@amazon.com + +## How to support Powertools for AWS Lambda (TypeScript)? + +### Becoming a reference customer + +Knowing which companies are using this library is important to help prioritize the project internally. If your company is using Powertools for AWS Lambda (TypeScript), you can request to have your name and logo added to the README file by raising a [Support Powertools for AWS Lambda (TypeScript) (become a reference)](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new?assignees=&labels=customer-reference&template=support_powertools.yml&title=%5BSupport+Lambda+Powertools%5D%3A+%3Cyour+organization+name%3E) issue. + +The following companies, among others, use Powertools: + +* [Hashnode](https://hashnode.com/) +* [Trek10](https://www.trek10.com/) +* [Elva](https://elva-group.com) +* [globaldatanet](https://globaldatanet.com/) +* [Bailey Nelson](https://www.baileynelson.com.au) +* [Perfect Post](https://www.perfectpost.fr) +* [Sennder](https://sennder.com/) +* [Certible](https://www.certible.com/) +* [tecRacer GmbH & Co. KG](https://www.tecracer.com/) +* [AppYourself](https://appyourself.net) +* [Alma Media](https://www.almamedia.fi) + +### Sharing your work + +Share what you did with Powertools for AWS Lambda (TypeScript) 💞💞. Blog post, workshops, presentation, sample apps and others. Check out what the community has already shared about Powertools for AWS Lambda (TypeScript) [here](https://docs.powertools.aws.dev/lambda/typescript/latest/we_made_this). + +### Using Lambda Layer + +This helps us understand who uses Powertools for AWS Lambda (TypeScript) in a non-intrusive way, and helps us gain future investments for other Powertools for AWS Lambda languages. When [using Layers](https://docs.powertools.aws.dev/lambda/typescript/latest/#lambda-layer), you can add Powertools as a dev dependency (or as part of your virtual env) to not impact the development process. + +## Credits + +Credits for the Lambda Powertools for AWS Lambda (TypeScript) idea go to [DAZN](https://github.com/getndazn) and their [DAZN Lambda Powertools](https://github.com/getndazn/dazn-lambda-powertools/). + +## License + +This library is licensed under the MIT-0 License. See the LICENSE file. \ No newline at end of file diff --git a/packages/parser/jest.config.cjs b/packages/parser/jest.config.cjs new file mode 100644 index 0000000000..e1dca130b9 --- /dev/null +++ b/packages/parser/jest.config.cjs @@ -0,0 +1,31 @@ +module.exports = { + displayName: { + name: 'Powertools for AWS Lambda (TypeScript) utility: PARSER', + color: 'blue', + }, + runner: 'groups', + preset: 'ts-jest', + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + transform: { + '^.+\\.ts?$': ['ts-jest', {tsconfig: './tests/tsconfig.json'}], + + }, + moduleFileExtensions: ['js', 'ts'], + collectCoverageFrom: ['**/src/**/*.ts', '!**/node_modules/**'], + testMatch: ['**/?(*.)+(spec|test).ts'], + roots: ['/src', '/tests'], + testPathIgnorePatterns: ['/node_modules/'], + testEnvironment: 'node', + coveragePathIgnorePatterns: ['/node_modules/', '/types'], + coverageThreshold: { + global: { + statements: 100, + branches: 100, + functions: 100, + lines: 100, + }, + }, + coverageReporters: ['json-summary', 'text', 'lcov'], +}; diff --git a/packages/parser/package.json b/packages/parser/package.json new file mode 100644 index 0000000000..52f6fc96fd --- /dev/null +++ b/packages/parser/package.json @@ -0,0 +1,113 @@ +{ + "name": "@aws-lambda-powertools/parser", + "version": "2.1.0", + "description": "The parser package for the Powertools for AWS Lambda (TypeScript) library.", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "npm run test:unit", + "test:unit": "jest --group=unit --detectOpenHandles --coverage --verbose", + "jest": "jest --detectOpenHandles --coverage --verbose", + "watch": "jest --watch", + "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": "npm run build:esm & npm run build:cjs", + "lint": "eslint --ext .ts,.js --no-error-on-unmatched-pattern .", + "lint-fix": "eslint --fix --ext .ts,.js --no-error-on-unmatched-pattern .", + "prepack": "node ../../.github/scripts/release_patch_package_json.js ." + }, + "lint-staged": { + "*.{js,ts}": "npm run lint-fix" + }, + "homepage": "https://github.com/aws-powertools/powertools-lambda-typescript/tree/main/packages/parser#readme", + "license": "MIT-0", + "type": "module", + "exports": { + ".": { + "require": { + "types": "./lib/cjs/index.d.ts", + "default": "./lib/cjs/index.js" + }, + "import": { + "types": "./lib/esm/index.d.ts", + "default": "./lib/esm/index.js" + } + }, + "./middleware": { + "require": "./lib/cjs/middleware/parser.js", + "import": "./lib/esm/middleware/parser.js" + }, + "./schemas": { + "require": "./lib/cjs/schemas/index.js", + "import": "./lib/esm/schemas/index.js" + }, + "./envelopes": { + "require": "./lib/cjs/envelopes/index.js", + "import": "./lib/esm/envelopes/index.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" + ], + "envelopes": [ + "./lib/cjs/envelopes/index.d.ts", + "./lib/esm/envelopes/index.d.ts" + ] + } + }, + "main": "./lib/cjs/index.js", + "types": "./lib/cjs/index.d.ts", + "files": [ + "lib" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/aws-powertools/powertools-lambda-typescript.git" + }, + "bugs": { + "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" + }, + "keywords": [ + "aws", + "lambda", + "powertools", + "parser", + "zod", + "parse", + "events", + "payloads", + "inputs", + "validate", + "validation", + "serverless", + "typescript", + "nodejs" + ], + "peerDependencies": { + "zod": ">=3.x" + }, + "devDependencies": { + "@anatine/zod-mock": "^3.13.3", + "@faker-js/faker": "^8.3.1" + } +} diff --git a/packages/parser/src/envelopes/apigw.ts b/packages/parser/src/envelopes/apigw.ts new file mode 100644 index 0000000000..064fe8477c --- /dev/null +++ b/packages/parser/src/envelopes/apigw.ts @@ -0,0 +1,49 @@ +import { Envelope } from './envelope.js'; +import { z, type ZodSchema } from 'zod'; +import { APIGatewayProxyEventSchema } from '../schemas/apigw.js'; +import type { ParsedResult } from '../types/parser.js'; +import { ParseError } from '../errors.js'; + +/** + * API Gateway envelope to extract data within body key + */ +export class ApiGatewayEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + return super.parse(APIGatewayProxyEventSchema.parse(data).body, schema); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult> { + const parsedEnvelope = APIGatewayProxyEventSchema.safeParse(data); + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse ApiGatewayEnvelope', + parsedEnvelope.error + ), + originalEvent: data, + }; + } + + const parsedBody = super.safeParse(parsedEnvelope.data.body, schema); + + if (!parsedBody.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse ApiGatewayEnvelope body', + parsedBody.error + ), + originalEvent: data, + }; + } + + return parsedBody; + } +} diff --git a/packages/parser/src/envelopes/apigwv2.ts b/packages/parser/src/envelopes/apigwv2.ts new file mode 100644 index 0000000000..d4abbc4738 --- /dev/null +++ b/packages/parser/src/envelopes/apigwv2.ts @@ -0,0 +1,49 @@ +import { z, type ZodSchema } from 'zod'; +import { APIGatewayProxyEventV2Schema } from '../schemas/apigwv2.js'; +import { Envelope } from './envelope.js'; +import type { ParsedResult } from '../types/index.js'; +import { ParseError } from '../errors.js'; + +/** + * API Gateway V2 envelope to extract data within body key + */ +export class ApiGatewayV2Envelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + return super.parse(APIGatewayProxyEventV2Schema.parse(data).body, schema); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = APIGatewayProxyEventV2Schema.safeParse(data); + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse API Gateway V2 envelope', + parsedEnvelope.error + ), + originalEvent: data, + }; + } + + const parsedBody = super.safeParse(parsedEnvelope.data.body, schema); + + if (!parsedBody.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse API Gateway V2 envelope body', + parsedBody.error + ), + originalEvent: data, + }; + } + + return parsedBody; + } +} diff --git a/packages/parser/src/envelopes/cloudwatch.ts b/packages/parser/src/envelopes/cloudwatch.ts new file mode 100644 index 0000000000..4778d9b508 --- /dev/null +++ b/packages/parser/src/envelopes/cloudwatch.ts @@ -0,0 +1,67 @@ +import { z, type ZodSchema } from 'zod'; +import { Envelope } from './envelope.js'; +import { CloudWatchLogsSchema } from '../schemas/index.js'; +import type { ParsedResult } from '../types/index.js'; +import { ParseError } from '../errors.js'; + +/** + * CloudWatch Envelope to extract a List of log records. + * + * The record's body parameter is a string (after being base64 decoded and gzipped), + * though it can also be a JSON encoded string. + * Regardless of its type it'll be parsed into a BaseModel object. + * + * Note: The record will be parsed the same way so if model is str + */ +export class CloudWatchEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = CloudWatchLogsSchema.parse(data); + + return parsedEnvelope.awslogs.data.logEvents.map((record) => { + return super.parse(record.message, schema); + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = CloudWatchLogsSchema.safeParse(data); + + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse CloudWatch envelope', + parsedEnvelope.error + ), + originalEvent: data, + }; + } + const parsedLogEvents: z.infer[] = []; + + for (const record of parsedEnvelope.data.awslogs.data.logEvents) { + const parsedMessage = super.safeParse(record.message, schema); + if (!parsedMessage.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse CloudWatch log event', + parsedMessage.error + ), + originalEvent: data, + }; + } else { + parsedLogEvents.push(parsedMessage.data); + } + } + + return { + success: true, + data: parsedLogEvents, + }; + } +} diff --git a/packages/parser/src/envelopes/dynamodb.ts b/packages/parser/src/envelopes/dynamodb.ts new file mode 100644 index 0000000000..59bf83a850 --- /dev/null +++ b/packages/parser/src/envelopes/dynamodb.ts @@ -0,0 +1,78 @@ +import { z, type ZodSchema } from 'zod'; +import { DynamoDBStreamSchema } from '../schemas/index.js'; +import type { ParsedResult, ParsedResultError } from '../types/index.js'; +import { Envelope } from './envelope.js'; +import { ParseError } from '../errors.js'; + +type DynamoDBStreamEnvelopeResponse = { + NewImage: z.infer; + OldImage: z.infer; +}; + +/** + * DynamoDB Stream Envelope to extract data within NewImage/OldImage + * + * Note: Values are the parsed models. Images' values can also be None, and + * length of the list is the record's amount in the original event. + */ +export class DynamoDBStreamEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): DynamoDBStreamEnvelopeResponse>[] { + const parsedEnvelope = DynamoDBStreamSchema.parse(data); + + return parsedEnvelope.Records.map((record) => { + return { + NewImage: super.parse(record.dynamodb.NewImage, schema), + OldImage: super.parse(record.dynamodb.OldImage, schema), + }; + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = DynamoDBStreamSchema.safeParse(data); + + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse DynamoDB Stream envelope', + parsedEnvelope.error + ), + originalEvent: data, + }; + } + const parsedLogEvents: DynamoDBStreamEnvelopeResponse>[] = []; + + for (const record of parsedEnvelope.data.Records) { + const parsedNewImage = super.safeParse(record.dynamodb.NewImage, schema); + const parsedOldImage = super.safeParse(record.dynamodb.OldImage, schema); + if (!parsedNewImage.success || !parsedOldImage.success) { + return { + success: false, + error: !parsedNewImage.success + ? new ParseError('Failed to parse NewImage', parsedNewImage.error) + : new ParseError( + 'Failed to parse OldImage', + (parsedOldImage as ParsedResultError).error + ), + originalEvent: data, + }; + } else { + parsedLogEvents.push({ + NewImage: parsedNewImage.data, + OldImage: parsedOldImage.data, + }); + } + } + + return { + success: true, + data: parsedLogEvents, + }; + } +} diff --git a/packages/parser/src/envelopes/envelope.ts b/packages/parser/src/envelopes/envelope.ts new file mode 100644 index 0000000000..9e0ae3a8cf --- /dev/null +++ b/packages/parser/src/envelopes/envelope.ts @@ -0,0 +1,77 @@ +import { z, type ZodSchema } from 'zod'; +import type { ParsedResult } from '../types/parser.js'; +import { ParseError } from '../errors.js'; + +export class Envelope { + /** + * Abstract function to parse the content of the envelope using provided schema. + * Both inputs are provided as unknown by the user. + * We expect the data to be either string that can be parsed to json or object. + * @internal + * @param data data to parse + * @param schema schema + */ + public static readonly parse = ( + data: unknown, + schema: T + ): z.infer => { + if (typeof data !== 'object' && typeof data !== 'string') { + throw new ParseError( + `Invalid data type for envelope. Expected string or object, got ${typeof data}` + ); + } + try { + if (typeof data === 'string') { + return schema.parse(JSON.parse(data)); + } else if (typeof data === 'object') { + return schema.parse(data); + } + } catch (e) { + throw new ParseError(`Failed to parse envelope`, e as Error); + } + }; + + /** + * Abstract function to safely parse the content of the envelope using provided schema. + * safeParse is used to avoid throwing errors, thus we catuch all errors and wrap them in the result. + * @param input + * @param schema + */ + public static readonly safeParse = ( + input: unknown, + schema: T + ): ParsedResult> => { + try { + if (typeof input !== 'object' && typeof input !== 'string') { + return { + success: false, + error: new ParseError( + `Invalid data type for envelope. Expected string or object, got ${typeof input}` + ), + originalEvent: input, + }; + } + + const parsed = schema.safeParse( + typeof input === 'string' ? JSON.parse(input) : input + ); + + return parsed.success + ? { + success: true, + data: parsed.data, + } + : { + success: false, + error: new ParseError(`Failed to parse envelope`, parsed.error), + originalEvent: input, + }; + } catch (e) { + return { + success: false, + error: new ParseError(`Failed to parse envelope`, e as Error), + originalEvent: input, + }; + } + }; +} diff --git a/packages/parser/src/envelopes/event-bridge.ts b/packages/parser/src/envelopes/event-bridge.ts new file mode 100644 index 0000000000..1659dd93ed --- /dev/null +++ b/packages/parser/src/envelopes/event-bridge.ts @@ -0,0 +1,50 @@ +import { Envelope } from './envelope.js'; +import { z, type ZodSchema } from 'zod'; +import { EventBridgeSchema } from '../schemas/index.js'; +import type { ParsedResult } from '../types/index.js'; +import { ParseError } from '../errors.js'; + +/** + * Envelope for EventBridge schema that extracts and parses data from the `detail` key. + */ +export class EventBridgeEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + return super.parse(EventBridgeSchema.parse(data).detail, schema); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = EventBridgeSchema.safeParse(data); + + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse EventBridge envelope', + parsedEnvelope.error + ), + originalEvent: data, + }; + } + + const parsedDetail = super.safeParse(parsedEnvelope.data.detail, schema); + + if (!parsedDetail.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse EventBridge envelope detail', + parsedDetail.error + ), + originalEvent: data, + }; + } + + return parsedDetail; + } +} diff --git a/packages/parser/src/envelopes/index.ts b/packages/parser/src/envelopes/index.ts new file mode 100644 index 0000000000..3d1487a7b0 --- /dev/null +++ b/packages/parser/src/envelopes/index.ts @@ -0,0 +1,13 @@ +export { ApiGatewayEnvelope } from './apigw.js'; +export { ApiGatewayV2Envelope } from './apigwv2.js'; +export { CloudWatchEnvelope } from './cloudwatch.js'; +export { DynamoDBStreamEnvelope } from './dynamodb.js'; +export { EventBridgeEnvelope } from './event-bridge.js'; +export { KafkaEnvelope } from './kafka.js'; +export { KinesisEnvelope } from './kinesis.js'; +export { KinesisFirehoseEnvelope } from './kinesis-firehose.js'; +export { LambdaFunctionUrlEnvelope } from './lambda.js'; +export { SnsEnvelope, SnsSqsEnvelope } from './sns.js'; +export { SqsEnvelope } from './sqs.js'; +export { VpcLatticeEnvelope } from './vpc-lattice.js'; +export { VpcLatticeV2Envelope } from './vpc-latticev2.js'; diff --git a/packages/parser/src/envelopes/kafka.ts b/packages/parser/src/envelopes/kafka.ts new file mode 100644 index 0000000000..8c8e481a07 --- /dev/null +++ b/packages/parser/src/envelopes/kafka.ts @@ -0,0 +1,87 @@ +import { z, type ZodSchema } from 'zod'; +import { Envelope } from './envelope.js'; +import { + KafkaMskEventSchema, + KafkaSelfManagedEventSchema, +} from '../schemas/kafka.js'; +import { ParsedResult, KafkaMskEvent } from '../types/index.js'; +import { ParseError } from '../errors.js'; + +/** + * Kafka event envelope to extract data within body key + * The record's body parameter is a string, though it can also be a JSON encoded string. + * Regardless of its type it'll be parsed into a BaseModel object. + * + * Note: Records will be parsed the same way so if model is str, + * all items in the list will be parsed as str and not as JSON (and vice versa) + */ + +export class KafkaEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + // manually fetch event source to deside between Msk or SelfManaged + const eventSource = (data as KafkaMskEvent)['eventSource']; + + const parsedEnvelope: + | z.infer + | z.infer = + eventSource === 'aws:kafka' + ? KafkaMskEventSchema.parse(data) + : KafkaSelfManagedEventSchema.parse(data); + + return Object.values(parsedEnvelope.records).map((topicRecord) => { + return topicRecord.map((record) => { + return super.parse(record.value, schema); + }); + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + // manually fetch event source to deside between Msk or SelfManaged + const eventSource = (data as KafkaMskEvent)['eventSource']; + + const parsedEnvelope = + eventSource === 'aws:kafka' + ? KafkaMskEventSchema.safeParse(data) + : KafkaSelfManagedEventSchema.safeParse(data); + + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse Kafka envelope', + parsedEnvelope.error + ), + originalEvent: data, + }; + } + const parsedRecords: z.infer[] = []; + + for (const topicRecord of Object.values(parsedEnvelope.data.records)) { + for (const record of topicRecord) { + const parsedRecord = super.safeParse(record.value, schema); + if (!parsedRecord.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse Kafka record', + parsedRecord.error + ), + originalEvent: data, + }; + } + parsedRecords.push(parsedRecord.data); + } + } + + return { + success: true, + data: parsedRecords, + }; + } +} diff --git a/packages/parser/src/envelopes/kinesis-firehose.ts b/packages/parser/src/envelopes/kinesis-firehose.ts new file mode 100644 index 0000000000..71535894c3 --- /dev/null +++ b/packages/parser/src/envelopes/kinesis-firehose.ts @@ -0,0 +1,69 @@ +import { z, type ZodSchema } from 'zod'; +import { Envelope } from './envelope.js'; +import { KinesisFirehoseSchema } from '../schemas/index.js'; +import type { ParsedResult } from '../types/index.js'; +import { ParseError } from '../errors.js'; + +/** + * Kinesis Firehose Envelope to extract array of Records + * + * The record's data parameter is a base64 encoded string which is parsed into a bytes array, + * though it can also be a JSON encoded string. + * Regardless of its type it'll be parsed into a BaseModel object. + * + * Note: Records will be parsed the same way so if model is str, + * all items in the list will be parsed as str and not as JSON (and vice versa) + * + * https://docs.aws.amazon.com/lambda/latest/dg/services-kinesisfirehose.html + */ +export class KinesisFirehoseEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = KinesisFirehoseSchema.parse(data); + + return parsedEnvelope.records.map((record) => { + return super.parse(record.data, schema); + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = KinesisFirehoseSchema.safeParse(data); + + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse Kinesis Firehose envelope', + parsedEnvelope.error + ), + originalEvent: data, + }; + } + const parsedRecords: z.infer[] = []; + + for (const record of parsedEnvelope.data.records) { + const parsedData = super.safeParse(record.data, schema); + if (!parsedData.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse Kinesis Firehose record', + parsedData.error + ), + originalEvent: data, + }; + } + parsedRecords.push(parsedData.data); + } + + return { + success: true, + data: parsedRecords, + }; + } +} diff --git a/packages/parser/src/envelopes/kinesis.ts b/packages/parser/src/envelopes/kinesis.ts new file mode 100644 index 0000000000..8ddfb3d34e --- /dev/null +++ b/packages/parser/src/envelopes/kinesis.ts @@ -0,0 +1,67 @@ +import { Envelope } from './envelope.js'; +import { z, type ZodSchema } from 'zod'; +import { KinesisDataStreamSchema } from '../schemas/kinesis.js'; +import type { ParsedResult } from '../types/index.js'; +import { ParseError } from '../errors.js'; + +/** + * Kinesis Data Stream Envelope to extract array of Records + * + * The record's data parameter is a base64 encoded string which is parsed into a bytes array, + * though it can also be a JSON encoded string. + * Regardless of its type it'll be parsed into a BaseModel object. + * + * Note: Records will be parsed the same way so if model is str, + * all items in the list will be parsed as str and not as JSON (and vice versa) + */ +export class KinesisEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = KinesisDataStreamSchema.parse(data); + + return parsedEnvelope.Records.map((record) => { + return super.parse(record.kinesis.data, schema); + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = KinesisDataStreamSchema.safeParse(data); + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse Kinesis Data Stream envelope', + parsedEnvelope.error + ), + originalEvent: data, + }; + } + + const parsedRecords: z.infer[] = []; + + for (const record of parsedEnvelope.data.Records) { + const parsedRecord = super.safeParse(record.kinesis.data, schema); + if (!parsedRecord.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse Kinesis Data Stream record', + parsedRecord.error + ), + originalEvent: data, + }; + } + parsedRecords.push(parsedRecord.data); + } + + return { + success: true, + data: parsedRecords, + }; + } +} diff --git a/packages/parser/src/envelopes/lambda.ts b/packages/parser/src/envelopes/lambda.ts new file mode 100644 index 0000000000..9d694cc517 --- /dev/null +++ b/packages/parser/src/envelopes/lambda.ts @@ -0,0 +1,52 @@ +import { Envelope } from './envelope.js'; +import { z, type ZodSchema } from 'zod'; +import { LambdaFunctionUrlSchema } from '../schemas/index.js'; +import type { ParsedResult } from '../types/index.js'; +import { ParseError } from '../errors.js'; + +/** + * Lambda function URL envelope to extract data within body key + */ +export class LambdaFunctionUrlEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = LambdaFunctionUrlSchema.parse(data); + + if (!parsedEnvelope.body) { + throw new Error('Body field of Lambda function URL event is undefined'); + } + + return super.parse(parsedEnvelope.body, schema); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = LambdaFunctionUrlSchema.safeParse(data); + + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError('Failed to parse Lambda function URL envelope'), + originalEvent: data, + }; + } + + const parsedBody = super.safeParse(parsedEnvelope.data.body, schema); + if (!parsedBody.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse Lambda function URL body', + parsedBody.error + ), + originalEvent: data, + }; + } + + return parsedBody; + } +} diff --git a/packages/parser/src/envelopes/sns.ts b/packages/parser/src/envelopes/sns.ts new file mode 100644 index 0000000000..50083d7ca7 --- /dev/null +++ b/packages/parser/src/envelopes/sns.ts @@ -0,0 +1,156 @@ +import { z, type ZodSchema } from 'zod'; +import { Envelope } from './envelope.js'; +import { SnsSchema, SnsSqsNotificationSchema } from '../schemas/sns.js'; +import { SqsSchema } from '../schemas/sqs.js'; +import type { ParsedResult } from '../types/index.js'; +import { ParseError } from '../errors.js'; + +/** + * SNS Envelope to extract array of Records + * + * The record's body parameter is a string, though it can also be a JSON encoded string. + * Regardless of its type it'll be parsed into a BaseModel object. + * + * Note: Records will be parsed the same way so if model is str, + * all items in the list will be parsed as str and npt as JSON (and vice versa) + */ +export class SnsEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = SnsSchema.parse(data); + + return parsedEnvelope.Records.map((record) => { + return super.parse(record.Sns.Message, schema); + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = SnsSchema.safeParse(data); + + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError( + `Failed to parse SNS envelope`, + parsedEnvelope.error + ), + originalEvent: data, + }; + } + + const parsedMessages: z.infer[] = []; + for (const record of parsedEnvelope.data.Records) { + const parsedMessage = super.safeParse(record.Sns.Message, schema); + if (!parsedMessage.success) { + return { + success: false, + error: new ParseError( + `Failed to parse SNS message`, + parsedMessage.error + ), + originalEvent: data, + }; + } + parsedMessages.push(parsedMessage.data); + } + + return { + success: true, + data: parsedMessages, + }; + } +} + +/** + * SNS plus SQS Envelope to extract array of Records + * + * Published messages from SNS to SQS has a slightly different payload. + * Since SNS payload is marshalled into `Record` key in SQS, we have to: + * + * 1. Parse SQS schema with incoming data + * 2. Unmarshall SNS payload and parse against SNS Notification schema not SNS/SNS Record + * 3. Finally, parse provided model against payload extracted + * + */ +export class SnsSqsEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = SqsSchema.parse(data); + + return parsedEnvelope.Records.map((record) => { + const snsNotification = SnsSqsNotificationSchema.parse( + JSON.parse(record.body) + ); + + return super.parse(snsNotification.Message, schema); + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = SqsSchema.safeParse(data); + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError( + `Failed to parse SQS envelope`, + parsedEnvelope.error + ), + originalEvent: data, + }; + } + + const parsedMessages: z.infer[] = []; + + // JSON.parse can throw an error, thus we catch it and return ParsedErrorResult + try { + for (const record of parsedEnvelope.data.Records) { + const snsNotification = SnsSqsNotificationSchema.safeParse( + JSON.parse(record.body) + ); + if (!snsNotification.success) { + return { + success: false, + error: new ParseError( + `Failed to parse SNS notification`, + snsNotification.error + ), + originalEvent: data, + }; + } + const parsedMessage = super.safeParse( + snsNotification.data.Message, + schema + ); + if (!parsedMessage.success) { + return { + success: false, + error: new ParseError( + `Failed to parse SNS message`, + parsedMessage.error + ), + originalEvent: data, + }; + } + parsedMessages.push(parsedMessage.data); + } + } catch (e) { + return { + success: false, + error: e as Error, + originalEvent: data, + }; + } + + return { success: true, data: parsedMessages }; + } +} diff --git a/packages/parser/src/envelopes/sqs.ts b/packages/parser/src/envelopes/sqs.ts new file mode 100644 index 0000000000..e5350365d4 --- /dev/null +++ b/packages/parser/src/envelopes/sqs.ts @@ -0,0 +1,62 @@ +import { z, type ZodSchema } from 'zod'; +import { SqsSchema } from '../schemas/sqs.js'; +import { Envelope } from './envelope.js'; +import type { ParsedResult } from '../types/index.js'; +import { ParseError } from '../errors.js'; + +/** + * SQS Envelope to extract array of Records + * + * The record's body parameter is a string, though it can also be a JSON encoded string. + * Regardless of its type it'll be parsed into a BaseModel object. + * + * Note: Records will be parsed the same way so if model is str, + * all items in the list will be parsed as str and npt as JSON (and vice versa) + */ +export class SqsEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = SqsSchema.parse(data); + + return parsedEnvelope.Records.map((record) => { + return super.parse(record.body, schema); + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = SqsSchema.safeParse(data); + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse Sqs Envelope', + parsedEnvelope.error + ), + originalEvent: data, + }; + } + + const parsedRecords: z.infer[] = []; + for (const record of parsedEnvelope.data.Records) { + const parsedRecord = super.safeParse(record.body, schema); + if (!parsedRecord.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse Sqs Record', + parsedRecord.error + ), + originalEvent: data, + }; + } + parsedRecords.push(parsedRecord.data); + } + + return { success: true, data: parsedRecords }; + } +} diff --git a/packages/parser/src/envelopes/vpc-lattice.ts b/packages/parser/src/envelopes/vpc-lattice.ts new file mode 100644 index 0000000000..1f157eac3d --- /dev/null +++ b/packages/parser/src/envelopes/vpc-lattice.ts @@ -0,0 +1,52 @@ +import { z, type ZodSchema } from 'zod'; +import { Envelope } from './envelope.js'; +import { VpcLatticeSchema } from '../schemas/index.js'; +import type { ParsedResult } from '../types/index.js'; +import { ParseError } from '../errors.js'; + +/** + * Amazon VPC Lattice envelope to extract data within body key + */ + +export class VpcLatticeEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = VpcLatticeSchema.parse(data); + + return super.parse(parsedEnvelope.body, schema); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = VpcLatticeSchema.safeParse(data); + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse VpcLattice envelope', + parsedEnvelope.error + ), + originalEvent: data, + }; + } + + const parsedBody = super.safeParse(parsedEnvelope.data.body, schema); + + if (!parsedBody.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse VpcLattice envelope body', + parsedBody.error + ), + originalEvent: data, + }; + } + + return parsedBody; + } +} diff --git a/packages/parser/src/envelopes/vpc-latticev2.ts b/packages/parser/src/envelopes/vpc-latticev2.ts new file mode 100644 index 0000000000..50a1b058f3 --- /dev/null +++ b/packages/parser/src/envelopes/vpc-latticev2.ts @@ -0,0 +1,51 @@ +import { Envelope } from './envelope.js'; +import { z, type ZodSchema } from 'zod'; +import { VpcLatticeV2Schema } from '../schemas/index.js'; +import type { ParsedResult } from '../types/index.js'; +import { ParseError } from '../errors.js'; + +/** + * Amazon VPC Lattice envelope to extract data within body key + */ +export class VpcLatticeV2Envelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = VpcLatticeV2Schema.parse(data); + + return super.parse(parsedEnvelope.body, schema); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = VpcLatticeV2Schema.safeParse(data); + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse VpcLatticeV2 envelope.', + parsedEnvelope.error + ), + originalEvent: data, + }; + } + + const parsedBody = super.safeParse(parsedEnvelope.data.body, schema); + + if (!parsedBody.success) { + return { + success: false, + error: new ParseError( + 'Failed to parse VpcLatticeV2 body.', + parsedBody.error + ), + originalEvent: data, + }; + } + + return parsedBody; + } +} diff --git a/packages/parser/src/errors.ts b/packages/parser/src/errors.ts new file mode 100644 index 0000000000..8e03bce5e9 --- /dev/null +++ b/packages/parser/src/errors.ts @@ -0,0 +1,25 @@ +/** + * Error thrown when a parsing error occurs. The cause of the error is included in the message, if possible. + */ +class ParseError extends Error { + /** + * we use the `cause` property, which is present in ES2022 or newer, to store the cause of the error. + * because we have to support Node 16.x, we need to add this property ourselves. + * We can remove this once we drop support for Node 16.x. + * see: https://github.com/aws-powertools/powertools-lambda-typescript/issues/2223 + * + * @see https://nodejs.org/api/errors.html#errors_error_cause + */ + public readonly cause: Error | undefined; + + public constructor(message: string, cause?: Error) { + const errorMessage = cause + ? `${message}. This error was caused by: ${cause.message}.` + : message; + super(errorMessage); + this.cause = cause; + this.name = 'ParseError'; + } +} + +export { ParseError }; diff --git a/packages/parser/src/index.ts b/packages/parser/src/index.ts new file mode 100644 index 0000000000..e181735190 --- /dev/null +++ b/packages/parser/src/index.ts @@ -0,0 +1,2 @@ +export { parser } from './parserDecorator.js'; +export { ParseError } from './errors.js'; diff --git a/packages/parser/src/middleware/parser.ts b/packages/parser/src/middleware/parser.ts new file mode 100644 index 0000000000..0c536b0aeb --- /dev/null +++ b/packages/parser/src/middleware/parser.ts @@ -0,0 +1,49 @@ +import { type MiddyLikeRequest } from '@aws-lambda-powertools/commons/types'; +import { type MiddlewareObj } from '@middy/core'; +import { type ZodSchema } from 'zod'; +import { type ParserOptions } from '../types/parser.js'; +import { parse } from '../parser.js'; + +/** + * A middiy middleware to parse your event. + * + * @exmaple + * ```typescript + * import { parser } from '@aws-lambda-powertools/parser/middleware'; + * import middy from '@middy/core'; + * import { sqsEnvelope } from '@aws-lambda-powertools/parser/envelopes/sqs;' + * + * const oderSchema = z.object({ + * id: z.number(), + * description: z.string(), + * quantity: z.number(), + * }); + * + * type Order = z.infer; + * + * export const handler = middy( + * async (event: Order, _context: unknown): Promise => { + * // event is validated as sqs message envelope + * // the body is unwrapped and parsed into object ready to use + * // you can now use event as Order in your code + * } + * ).use(parser({ schema: oderSchema, envelope: sqsEnvelope })); + * ``` + * + * @param options + */ +const parser = ( + options: ParserOptions +): MiddlewareObj => { + const before = (request: MiddyLikeRequest): void => { + const { schema, envelope, safeParse } = options; + + request.event = parse(request.event, envelope, schema, safeParse); + }; + + return { + before, + }; +}; + +export { parser }; diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts new file mode 100644 index 0000000000..b7537de77a --- /dev/null +++ b/packages/parser/src/parser.ts @@ -0,0 +1,74 @@ +import type { ParsedResult, Envelope } from './types/index.js'; +import { z, type ZodSchema } from 'zod'; +import { ParseError } from './errors.js'; + +/** + * Parse the data using the provided schema, envelope and safeParse flag + * + * @example + * ```typescript + * import { z } from 'zod'; + * import type { SqsEvent, ParsedResult } from '@aws-lambda-powertools/parser/types'; + * import { SqsEnvelope } from '@aws-lambda-powertools/parser/types/envelopes'; + * import { parse } from '@aws-lambda-powertools/parser'; + * + * const Order = z.object({ + * orderId: z.string(), + * description: z.string(), + * }); + * + * const handler = async (event: SqsEvent, context: unknown): Promise => { + * const parsedEvent = parse(event, SqsEnvelope, Order); + * + * const parsedSafe: ParsedResult = parse(event, SqsEnvelope, Order, true) + * } + * @param data the data to parse + * @param envelope the envelope to use, can be undefined + * @param schema the schema to use + * @param safeParse whether to use safeParse or not, if true it will return a ParsedResult with the original event if the parsing fails + */ +const parse = ( + data: z.infer, + envelope: E | undefined, + schema: T, + safeParse?: boolean +): ParsedResult | z.infer => { + if (envelope && safeParse) { + return envelope.safeParse(data, schema); + } + if (envelope) { + return envelope.parse(data, schema); + } + if (safeParse) { + return safeParseSchema(data, schema); + } + try { + return schema.parse(data); + } catch (e) { + throw new ParseError('Failed to parse schema', e as Error); + } +}; + +/** + * Parse the data safely using the provided schema. + * This function will not throw an error if the parsing fails, instead it will return a ParsedResultError with the original event. + * Otherwise, it will return ParsedResultSuccess with the parsed data. + * @param data the data to parse + * @param schema the zod schema to use + */ +const safeParseSchema = ( + data: z.infer, + schema: T +): ParsedResult => { + const result = schema.safeParse(data); + + return result.success + ? result + : { + success: false, + error: new ParseError('Failed to parse schema safely', result.error), + originalEvent: data, + }; +}; + +export { parse }; diff --git a/packages/parser/src/parserDecorator.ts b/packages/parser/src/parserDecorator.ts new file mode 100644 index 0000000000..0572d3a22e --- /dev/null +++ b/packages/parser/src/parserDecorator.ts @@ -0,0 +1,94 @@ +import type { HandlerMethodDecorator } from '@aws-lambda-powertools/commons/types'; +import type { Context, Handler } from 'aws-lambda'; +import { ZodSchema, z } from 'zod'; +import { parse } from './parser.js'; +import type { ParserOptions, ParsedResult } from './types/index.js'; + +/** + * A decorator to parse your event. + * + * @example + * ```typescript + * import { z } from 'zod'; + * import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; + * import type { SqSEvent } from '@aws-lambda-powertools/parser/types; + * import { parser } from '@aws-lambda-powertools/parser'; + * import { SqsEnvelope } from '@aws-lambda-powertools/parser/envelopes'; + * + * const Order = z.object({ + * orderId: z.string(), + * description: z.string(), + * }); + * + * class Lambda implements LambdaInterface { + * + * ⁣@parser({ envelope: SqsEnvelope, schema: OrderSchema }) + * public async handler(event: Order, _context: Context): Promise { + * // sqs event is parsed and the payload is extracted and parsed + * // apply business logic to your Order event + * const res = processOrder(event); + * return res; + * } + * } + * + * ``` + * + * In case you want to parse the event and handle the error, you can use the safeParse option. + * The safeParse option will return an object with the parsed event and an error object if the parsing fails. + * + * @example + * ```typescript + * + * import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; + * import type { SqSEvent, ParsedResult } from '@aws-lambda-powertools/parser/types; + * import { z } from 'zod'; + * import { parser } from '@aws-lambda-powertools/parser'; + * import { SqsEnvelope } from '@aws-lambda-powertools/parser/envelopes'; + * + * + * const Order = z.object({ + * orderId: z.string(), + * description: z.string(), + * } + * + * class Lambda implements LambdaInterface { + * + * ⁣git@parser({ envelope: SqsEnvelope, schema: OrderSchema, safeParse: true }) + * public async handler(event: ParsedResult, _context: unknown): Promise { + * if (event.success) { + * // event.data is the parsed event object of type Order + * } else { + * // event.error is the error object, you can inspect and recover + * // event.originalEvent is the original event that failed to parse + * } + * } + * } + * ``` + * + * @param options Configure the parser with the `schema`, `envelope` and whether to `safeParse` or not + */ +export const parser = ( + options: ParserOptions +): HandlerMethodDecorator => { + return (_target, _propertyKey, descriptor) => { + const original = descriptor.value!; + + const { schema, envelope, safeParse } = options; + + descriptor.value = async function ( + this: Handler, + event: unknown, + context: Context, + callback + ) { + const parsedEvent: ParsedResult< + typeof event, + z.infer + > = parse(event, envelope, schema, safeParse); + + return original.call(this, parsedEvent, context, callback); + }; + + return descriptor; + }; +}; diff --git a/packages/parser/src/schemas/alb.ts b/packages/parser/src/schemas/alb.ts new file mode 100644 index 0000000000..54d5de4fed --- /dev/null +++ b/packages/parser/src/schemas/alb.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; + +const AlbSchema = z.object({ + httpMethod: z.string(), + path: z.string(), + body: z.string(), + isBase64Encoded: z.boolean(), + headers: z.record(z.string(), z.string()).optional(), + queryStringParameters: z.record(z.string(), z.string()).optional(), + requestContext: z.object({ + elb: z.object({ + targetGroupArn: z.string(), + }), + }), +}); + +const AlbMultiValueHeadersSchema = AlbSchema.extend({ + multiValueHeaders: z.record(z.string(), z.array(z.string())), + multiValueQueryStringParameters: z.record(z.string(), z.array(z.string())), +}); + +export { AlbSchema, AlbMultiValueHeadersSchema }; diff --git a/packages/parser/src/schemas/apigw.ts b/packages/parser/src/schemas/apigw.ts new file mode 100644 index 0000000000..72878de03a --- /dev/null +++ b/packages/parser/src/schemas/apigw.ts @@ -0,0 +1,110 @@ +import { z } from 'zod'; + +const APIGatewayCert = z.object({ + clientCertPem: z.string(), + subjectDN: z.string(), + issuerDN: z.string(), + serialNumber: z.string(), + validity: z.object({ + notBefore: z.string(), + notAfter: z.string(), + }), +}); + +const APIGatewayEventIdentity = z.object({ + accessKey: z.string().nullish(), + accountId: z.string().nullish(), + apiKey: z.string().nullish(), + apiKeyId: z.string().nullish(), + caller: z.string().nullish(), + cognitoAuthenticationProvider: z.string().nullish(), + cognitoAuthenticationType: z.string().nullish(), + cognitoIdentityId: z.string().nullish(), + cognitoIdentityPoolId: z.string().nullish(), + principalOrgId: z.string().nullish(), + sourceIp: z.string().ip().optional(), + user: z.string().nullish(), + userAgent: z.string().nullish(), + userArn: z.string().nullish(), + clientCert: APIGatewayCert.nullish(), +}); + +const APIGatewayEventRequestContext = z + .object({ + accountId: z.string(), + apiId: z.string(), + authorizer: z + .object({ + claims: z.record(z.string(), z.any()).nullish(), + scopes: z.array(z.string()).nullish(), + }) + .nullish(), + stage: z.string(), + protocol: z.string(), + identity: APIGatewayEventIdentity, + requestId: z.string(), + requestTime: z.string(), + requestTimeEpoch: z.number(), + resourceId: z.string().nullish(), + resourcePath: z.string(), + domainName: z.string().nullish(), + domainPrefix: z.string().nullish(), + extendedRequestId: z.string().nullish(), + httpMethod: z.enum([ + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE', + 'HEAD', + 'OPTIONS', + ]), + path: z.string(), + connectedAt: z.number().nullish(), + connectionId: z.string().nullish(), + eventType: z.enum(['CONNECT', 'MESSAGE', 'DISCONNECT']).nullish(), + messageDirection: z.string().nullish(), + messageId: z.string().nullish(), + routeKey: z.string().nullish(), + operationName: z.string().nullish(), + }) + .refine( + (input) => { + return ( + !input.messageId || (input.messageId && input.eventType === 'MESSAGE') + ); + }, + { + message: 'messageId is available only when `eventType` is MESSAGE', + } + ); + +const APIGatewayProxyEventSchema = z.object({ + version: z.string().optional(), + authorizationToken: z.string().optional(), + identitySource: z.string().optional(), + methodArn: z.string().optional(), + type: z.enum(['TOKEN', 'REQUEST']).optional(), + resource: z.string(), + path: z.string(), + httpMethod: z.enum([ + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE', + 'HEAD', + 'OPTIONS', + ]), + headers: z.record(z.string()).optional(), + queryStringParameters: z.record(z.string()).optional(), + multiValueHeaders: z.record(z.array(z.string())).optional(), + multiValueQueryStringParameters: z.record(z.array(z.string())).optional(), + requestContext: APIGatewayEventRequestContext, + pathParameters: z.record(z.string()).optional().nullish(), + stageVariables: z.record(z.string()).optional().nullish(), + isBase64Encoded: z.boolean().optional(), + body: z.string().optional(), +}); + +export { APIGatewayProxyEventSchema, APIGatewayCert }; diff --git a/packages/parser/src/schemas/apigwv2.ts b/packages/parser/src/schemas/apigwv2.ts new file mode 100644 index 0000000000..ec5cfb6156 --- /dev/null +++ b/packages/parser/src/schemas/apigwv2.ts @@ -0,0 +1,71 @@ +import { z } from 'zod'; +import { APIGatewayCert } from './apigw.js'; + +const RequestContextV2Authorizer = z.object({ + jwt: z + .object({ + claims: z.record(z.string(), z.any()), + scopes: z.array(z.string()).optional(), + }) + .optional(), + iam: z + .object({ + accessKey: z.string().optional(), + accountId: z.string().optional(), + callerId: z.string().optional(), + principalOrgId: z.string().optional(), + userArn: z.string().optional(), + userId: z.string().optional(), + cognitoIdentity: z.object({ + amr: z.array(z.string()), + identityId: z.string(), + identityPoolId: z.string(), + }), + }) + .optional(), + lambda: z.record(z.string(), z.any()).optional(), +}); + +const RequestContextV2Http = z.object({ + method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']), + path: z.string(), + protocol: z.string(), + sourceIp: z.string().ip(), + userAgent: z.string(), +}); + +const RequestContextV2 = z.object({ + accountId: z.string(), + apiId: z.string(), + authorizer: RequestContextV2Authorizer.optional(), + authentication: z + .object({ + clientCert: APIGatewayCert.optional(), + }) + .optional(), + domainName: z.string(), + domainPrefix: z.string(), + http: RequestContextV2Http, + requestId: z.string(), + routeKey: z.string(), + stage: z.string(), + time: z.string(), + timeEpoch: z.number(), +}); + +const APIGatewayProxyEventV2Schema = z.object({ + version: z.string(), + routeKey: z.string(), + rawPath: z.string(), + rawQueryString: z.string(), + cookies: z.array(z.string()).optional(), + headers: z.record(z.string()), + queryStringParameters: z.record(z.string()).optional(), + pathParameters: z.record(z.string()).optional().nullish(), + stageVariables: z.record(z.string()).optional().nullish(), + requestContext: RequestContextV2, + body: z.string().optional(), + isBase64Encoded: z.boolean(), +}); + +export { APIGatewayProxyEventV2Schema }; diff --git a/packages/parser/src/schemas/cloudformation-custom-resource.ts b/packages/parser/src/schemas/cloudformation-custom-resource.ts new file mode 100644 index 0000000000..84369dbdae --- /dev/null +++ b/packages/parser/src/schemas/cloudformation-custom-resource.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; + +const CloudFormationCustomResourceBaseSchema = z.object({ + ServiceToken: z.string(), + ResponseURL: z.string().url(), + StackId: z.string(), + RequestId: z.string(), + LogicalResourceId: z.string(), + ResourceType: z.string(), + ResourceProperties: z.record(z.any()), +}); + +const CloudFormationCustomResourceCreateSchema = + CloudFormationCustomResourceBaseSchema.merge( + z.object({ + RequestType: z.literal('Create'), + }) + ); + +const CloudFormationCustomResourceDeleteSchema = + CloudFormationCustomResourceBaseSchema.merge( + z.object({ + RequestType: z.literal('Delete'), + }) + ); + +const CloudFormationCustomResourceUpdateSchema = + CloudFormationCustomResourceBaseSchema.merge( + z.object({ + RequestType: z.literal('Update'), + OldResourceProperties: z.record(z.any()), + }) + ); + +export { + CloudFormationCustomResourceCreateSchema, + CloudFormationCustomResourceDeleteSchema, + CloudFormationCustomResourceUpdateSchema, +}; diff --git a/packages/parser/src/schemas/cloudwatch.ts b/packages/parser/src/schemas/cloudwatch.ts new file mode 100644 index 0000000000..2694507b04 --- /dev/null +++ b/packages/parser/src/schemas/cloudwatch.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; +import { gunzipSync } from 'node:zlib'; + +const CloudWatchLogEventSchema = z.object({ + id: z.string(), + timestamp: z.number(), + message: z.string(), +}); + +const CloudWatchLogsDecodeSchema = z.object({ + messageType: z.string(), + owner: z.string(), + logGroup: z.string(), + logStream: z.string(), + subscriptionFilters: z.array(z.string()), + logEvents: z.array(CloudWatchLogEventSchema), +}); + +const decompressRecordToJSON = ( + data: string +): z.infer => { + const uncompressed = gunzipSync(Buffer.from(data, 'base64')).toString('utf8'); + + return CloudWatchLogsDecodeSchema.parse(JSON.parse(uncompressed)); +}; + +const CloudWatchLogsSchema = z.object({ + awslogs: z.object({ + data: z.string().transform((data) => decompressRecordToJSON(data)), + }), +}); + +export { + CloudWatchLogsSchema, + CloudWatchLogsDecodeSchema, + decompressRecordToJSON, + CloudWatchLogEventSchema, +}; diff --git a/packages/parser/src/schemas/dynamodb.ts b/packages/parser/src/schemas/dynamodb.ts new file mode 100644 index 0000000000..011ff85f8a --- /dev/null +++ b/packages/parser/src/schemas/dynamodb.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; + +const DynamoDBStreamChangeRecord = z.object({ + ApproximateCreationDateTime: z.number().optional(), + Keys: z.record(z.string(), z.record(z.string(), z.any())), + NewImage: z.record(z.string(), z.any()).optional(), + OldImage: z.record(z.string(), z.any()).optional(), + SequenceNumber: z.string(), + SizeBytes: z.number(), + StreamViewType: z.enum([ + 'NEW_IMAGE', + 'OLD_IMAGE', + 'NEW_AND_OLD_IMAGES', + 'KEYS_ONLY', + ]), +}); + +const UserIdentity = z.object({ + type: z.enum(['Service']), + principalId: z.literal('dynamodb.amazonaws.com'), +}); + +const DynamoDBStreamRecord = z.object({ + eventID: z.string(), + eventName: z.enum(['INSERT', 'MODIFY', 'REMOVE']), + eventVersion: z.string(), + eventSource: z.literal('aws:dynamodb'), + awsRegion: z.string(), + eventSourceARN: z.string(), + dynamodb: DynamoDBStreamChangeRecord, + userIdentity: UserIdentity.optional(), +}); + +const DynamoDBStreamSchema = z.object({ + Records: z.array(DynamoDBStreamRecord), +}); + +export { + DynamoDBStreamSchema, + DynamoDBStreamRecord, + DynamoDBStreamChangeRecord, + UserIdentity, +}; diff --git a/packages/parser/src/schemas/eventbridge.ts b/packages/parser/src/schemas/eventbridge.ts new file mode 100644 index 0000000000..9c509e5c03 --- /dev/null +++ b/packages/parser/src/schemas/eventbridge.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +const EventBridgeSchema = z.object({ + version: z.string(), + id: z.string(), + source: z.string(), + account: z.string(), + time: z.string().datetime(), + region: z.string(), + resources: z.array(z.string()), + 'detail-type': z.string(), + detail: z.unknown(), + 'replay-name': z.string().optional(), +}); + +export { EventBridgeSchema }; diff --git a/packages/parser/src/schemas/index.ts b/packages/parser/src/schemas/index.ts new file mode 100644 index 0000000000..ff56350195 --- /dev/null +++ b/packages/parser/src/schemas/index.ts @@ -0,0 +1,33 @@ +export { AlbSchema, AlbMultiValueHeadersSchema } from './alb.js'; +export { APIGatewayProxyEventSchema } from './apigw.js'; +export { APIGatewayProxyEventV2Schema } from './apigwv2.js'; +export { + CloudFormationCustomResourceCreateSchema, + CloudFormationCustomResourceDeleteSchema, + CloudFormationCustomResourceUpdateSchema, +} from './cloudformation-custom-resource.js'; +export { + CloudWatchLogEventSchema, + CloudWatchLogsDecodeSchema, + CloudWatchLogsSchema, +} from './cloudwatch.js'; +export { DynamoDBStreamSchema } from './dynamodb.js'; +export { EventBridgeSchema } from './eventbridge.js'; +export { KafkaMskEventSchema, KafkaSelfManagedEventSchema } from './kafka.js'; +export { KinesisDataStreamSchema } from './kinesis.js'; +export { + KinesisFirehoseSchema, + KinesisFirehoseSqsSchema, +} from './kinesis-firehose.js'; +export { LambdaFunctionUrlSchema } from './lambda.js'; +export { + S3SqsEventNotificationSchema, + S3EventNotificationEventBridgeSchema, + S3ObjectLambdaEventSchema, + S3Schema, +} from './s3.js'; +export { SesSchema } from './ses.js'; +export { SnsSchema } from './sns.js'; +export { SqsSchema } from './sqs.js'; +export { VpcLatticeSchema } from './vpc-lattice.js'; +export { VpcLatticeV2Schema } from './vpc-latticev2.js'; diff --git a/packages/parser/src/schemas/kafka.ts b/packages/parser/src/schemas/kafka.ts new file mode 100644 index 0000000000..08bd5d03ab --- /dev/null +++ b/packages/parser/src/schemas/kafka.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; + +const KafkaRecordSchema = z.object({ + topic: z.string(), + partition: z.number(), + offset: z.number(), + timestamp: z.number(), + timestampType: z.string(), + key: z.string().transform((key) => { + return Buffer.from(key, 'base64').toString(); + }), + value: z.string().transform((value) => { + return Buffer.from(value, 'base64').toString(); + }), + headers: z.array( + z.record( + z.string(), + z.array(z.number()).transform((value) => { + return String.fromCharCode(...value); + }) + ) + ), +}); + +const KafkaBaseEventSchema = z.object({ + bootstrapServers: z + .string() + .transform((bootstrapServers) => { + return bootstrapServers ? bootstrapServers.split(',') : undefined; + }) + .nullish(), + records: z.record(z.string(), z.array(KafkaRecordSchema)), +}); + +const KafkaSelfManagedEventSchema = KafkaBaseEventSchema.extend({ + eventSource: z.literal('aws:SelfManagedKafka'), +}); + +const KafkaMskEventSchema = KafkaBaseEventSchema.extend({ + eventSource: z.literal('aws:kafka'), + eventSourceArn: z.string(), +}); + +export { KafkaSelfManagedEventSchema, KafkaMskEventSchema, KafkaRecordSchema }; diff --git a/packages/parser/src/schemas/kinesis-firehose.ts b/packages/parser/src/schemas/kinesis-firehose.ts new file mode 100644 index 0000000000..e541cc3f65 --- /dev/null +++ b/packages/parser/src/schemas/kinesis-firehose.ts @@ -0,0 +1,51 @@ +import { z } from 'zod'; +import { SqsRecordSchema } from './sqs.js'; + +const KinesisRecordMetaData = z.object({ + shardId: z.string(), + partitionKey: z.string(), + approximateArrivalTimestamp: z.number().positive(), + sequenceNumber: z.string(), + subsequenceNumber: z.number(), +}); + +const KinesisFireHoseRecordBase = z.object({ + recordId: z.string(), + approximateArrivalTimestamp: z.number().positive(), + kinesisRecordMetaData: KinesisRecordMetaData.optional(), +}); + +const KinesisFireHoseBaseSchema = z.object({ + invocationId: z.string(), + deliveryStreamArn: z.string(), + region: z.string(), + sourceKinesisStreamArn: z.string().optional(), +}); + +const KinesisFirehoseRecord = KinesisFireHoseRecordBase.extend({ + data: z + .string() + .transform((data) => Buffer.from(data, 'base64').toString('utf8')), +}); + +const KinesisFirehoseSqsRecord = KinesisFireHoseRecordBase.extend({ + data: z.string().transform((data) => { + try { + return SqsRecordSchema.parse( + JSON.parse(Buffer.from(data, 'base64').toString('utf8')) + ); + } catch (e) { + return data; + } + }), +}); + +const KinesisFirehoseSchema = KinesisFireHoseBaseSchema.extend({ + records: z.array(KinesisFirehoseRecord), +}); + +const KinesisFirehoseSqsSchema = KinesisFireHoseBaseSchema.extend({ + records: z.array(KinesisFirehoseSqsRecord), +}); + +export { KinesisFirehoseSchema, KinesisFirehoseSqsSchema }; diff --git a/packages/parser/src/schemas/kinesis.ts b/packages/parser/src/schemas/kinesis.ts new file mode 100644 index 0000000000..fbd734297e --- /dev/null +++ b/packages/parser/src/schemas/kinesis.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; +import { gunzipSync } from 'node:zlib'; + +const KinesisDataStreamRecordPayload = z.object({ + kinesisSchemaVersion: z.string(), + partitionKey: z.string(), + sequenceNumber: z.string(), + approximateArrivalTimestamp: z.number(), + data: z.string().transform((data) => { + const decompresed = decompress(data); + const decoded = Buffer.from(data, 'base64').toString('utf-8'); + try { + // If data was not compressed, try to parse it as JSON otherwise it must be string + return decompresed === data ? JSON.parse(decoded) : decompresed; + } catch (e) { + return decoded; + } + }), +}); + +const decompress = (data: string): string => { + try { + return JSON.parse(gunzipSync(Buffer.from(data, 'base64')).toString('utf8')); + } catch (e) { + return data; + } +}; + +const KinesisDataStreamRecord = z.object({ + eventSource: z.literal('aws:kinesis'), + eventVersion: z.string(), + eventID: z.string(), + eventName: z.literal('aws:kinesis:record'), + invokeIdentityArn: z.string(), + eventSourceARN: z.string(), + kinesis: KinesisDataStreamRecordPayload, +}); + +const KinesisDataStreamSchema = z.object({ + Records: z.array(KinesisDataStreamRecord), +}); + +export { + KinesisDataStreamSchema, + KinesisDataStreamRecord, + KinesisDataStreamRecordPayload, +}; diff --git a/packages/parser/src/schemas/lambda.ts b/packages/parser/src/schemas/lambda.ts new file mode 100644 index 0000000000..e8e72be255 --- /dev/null +++ b/packages/parser/src/schemas/lambda.ts @@ -0,0 +1,14 @@ +import { APIGatewayProxyEventV2Schema } from './apigwv2.js'; + +/** + * Lambda Function URL follows the API Gateway HTTP APIs Payload Format Version 2.0. + * + * Keys related to API Gateway features not available in Function URL use a sentinel value (e.g.`routeKey`, `stage`). + * Documentation: + * - https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html + * - https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads + * + */ +const LambdaFunctionUrlSchema = APIGatewayProxyEventV2Schema.extend({}); + +export { LambdaFunctionUrlSchema }; diff --git a/packages/parser/src/schemas/s3.ts b/packages/parser/src/schemas/s3.ts new file mode 100644 index 0000000000..558776ffa5 --- /dev/null +++ b/packages/parser/src/schemas/s3.ts @@ -0,0 +1,164 @@ +import { z } from 'zod'; +import { EventBridgeSchema } from './eventbridge.js'; +import { SqsRecordSchema } from './sqs.js'; + +const S3Identity = z.object({ + principalId: z.string(), +}); + +const S3RequestParameters = z.object({ + sourceIPAddress: z.string().ip(), +}); + +const S3ResponseElements = z.object({ + 'x-amz-request-id': z.string(), + 'x-amz-id-2': z.string(), +}); + +const S3Message = z.object({ + s3SchemaVersion: z.string(), + configurationId: z.string(), + object: z.object({ + key: z.string(), + size: z.number().optional(), + urlDecodedKey: z.string().optional(), + eTag: z.string().optional(), + sequencer: z.string(), + versionId: z.optional(z.string()), + }), + bucket: z.object({ + name: z.string(), + ownerIdentity: S3Identity, + arn: z.string(), + }), +}); + +const S3EventRecordGlacierEventData = z.object({ + restoreEventData: z.object({ + lifecycleRestorationExpiryTime: z.string(), + lifecycleRestoreStorageClass: z.string(), + }), +}); + +const S3RecordSchema = z + .object({ + eventVersion: z.string(), + eventSource: z.literal('aws:s3'), + awsRegion: z.string(), + eventTime: z.string().datetime(), + eventName: z.string(), + userIdentity: S3Identity, + requestParameters: S3RequestParameters, + responseElements: S3ResponseElements, + s3: S3Message, + glacierEventData: z.optional(S3EventRecordGlacierEventData), + }) + .refine((value) => { + return ( + (!value.eventName.includes('ObjectRemoved') && + value.s3.object.size === undefined) || + value.s3.object.eTag === undefined, + { + message: + 'S3 event notification with ObjectRemoved event name must have size or eTag defined', + } + ); + }); + +const S3EventNotificationEventBridgeDetailSchema = z.object({ + version: z.string(), + bucket: z.object({ + name: z.string(), + }), + object: z.object({ + key: z.string(), + size: z.number().nonnegative().optional(), + etag: z.string(), + 'version-id': z.string().optional(), + sequencer: z.string().optional(), + }), + 'request-id': z.string(), + requester: z.string(), + 'source-ip-address': z.string().ip().optional(), + reason: z.string().optional(), + 'deletion-type': z.string().optional(), + 'restore-expiry-time': z.string().optional(), + 'source-storage-class': z.string().optional(), + 'destination-storage-class': z.string().optional(), + 'destination-access-tier': z.string().optional(), +}); + +const S3EventNotificationEventBridgeSchema = EventBridgeSchema.extend({ + detail: S3EventNotificationEventBridgeDetailSchema, +}); + +const S3Schema = z.object({ + Records: z.array(S3RecordSchema), +}); + +const S3SqsEventNotificationRecordSchema = SqsRecordSchema.extend({ + body: z.string(), +}); + +const S3SqsEventNotificationSchema = z.object({ + Records: z.array(S3SqsEventNotificationRecordSchema), +}); + +const S3ObjectContext = z.object({ + inputS3Url: z.string().url(), + outputRoute: z.string(), + outputToken: z.string(), +}); + +const S3ObjectConfiguration = z.object({ + accessPointArn: z.string(), + supportingAccessPointArn: z.string(), + payload: z.union([z.string(), z.object({})]), +}); + +const S3ObjectUserRequest = z.object({ + url: z.string(), + headers: z.record(z.string(), z.string()), +}); + +const S3ObjectSessionContext = z.object({ + sessionIssuer: z.object({ + type: z.string(), + userName: z.string().optional(), + principalId: z.string(), + arn: z.string(), + accountId: z.string(), + }), + attributes: z.object({ + creationDate: z.string(), + mfaAuthenticated: z + .union([z.boolean(), z.literal('true'), z.literal('false')]) + .transform((value) => value === true || value === 'true'), + }), +}); + +const S3ObjectUserIdentity = z.object({ + type: z.string(), + accountId: z.string(), + accessKeyId: z.string(), + userName: z.string().optional(), + principalId: z.string(), + arn: z.string(), + sessionContext: S3ObjectSessionContext.optional(), +}); + +const S3ObjectLambdaEventSchema = z.object({ + xAmzRequestId: z.string(), + getObjectContext: S3ObjectContext, + configuration: S3ObjectConfiguration, + userRequest: S3ObjectUserRequest, + userIdentity: S3ObjectUserIdentity, + protocolVersion: z.string(), +}); + +export { + S3Schema, + S3EventNotificationEventBridgeSchema, + S3SqsEventNotificationSchema, + S3ObjectLambdaEventSchema, +}; diff --git a/packages/parser/src/schemas/ses.ts b/packages/parser/src/schemas/ses.ts new file mode 100644 index 0000000000..254d3de255 --- /dev/null +++ b/packages/parser/src/schemas/ses.ts @@ -0,0 +1,65 @@ +import { z } from 'zod'; + +const SesReceiptVerdict = z.object({ + status: z.enum(['PASS', 'FAIL', 'GRAY', 'PROCESSING_FAILED']), +}); + +const SesReceipt = z.object({ + timestamp: z.string().datetime(), + processingTimeMillis: z.number().int().positive(), + recipients: z.array(z.string()), + spamVerdict: SesReceiptVerdict, + virusVerdict: SesReceiptVerdict, + spfVerdict: SesReceiptVerdict, + dmarcVerdict: SesReceiptVerdict, + dkimVerdict: SesReceiptVerdict, + dmarcPolicy: z.enum(['none', 'quarantine', 'reject']), + action: z.object({ + type: z.enum(['Lambda']), + invocationType: z.literal('Event'), + functionArn: z.string(), + }), +}); + +const SesMail = z.object({ + timestamp: z.string().datetime(), + source: z.string(), + messageId: z.string(), + destination: z.array(z.string()), + headersTruncated: z.boolean(), + headers: z.array( + z.object({ + name: z.string(), + value: z.string(), + }) + ), + commonHeaders: z.object({ + from: z.array(z.string()), + to: z.array(z.string()), + cc: z.array(z.string()).optional(), + bcc: z.array(z.string()).optional(), + sender: z.array(z.string()).optional(), + 'reply-to': z.array(z.string()).optional(), + returnPath: z.string(), + messageId: z.string(), + date: z.string(), + subject: z.string(), + }), +}); + +const SesMessage = z.object({ + mail: SesMail, + receipt: SesReceipt, +}); + +const SesRecordSchema = z.object({ + eventSource: z.literal('aws:ses'), + eventVersion: z.string(), + ses: SesMessage, +}); + +const SesSchema = z.object({ + Records: z.array(SesRecordSchema), +}); + +export { SesSchema, SesRecordSchema }; diff --git a/packages/parser/src/schemas/sns.ts b/packages/parser/src/schemas/sns.ts new file mode 100644 index 0000000000..862306a66b --- /dev/null +++ b/packages/parser/src/schemas/sns.ts @@ -0,0 +1,49 @@ +import { z } from 'zod'; + +const SnsMsgAttribute = z.object({ + Type: z.string(), + Value: z.string(), +}); + +const SnsNotificationSchema = z.object({ + Subject: z.string().optional(), + TopicArn: z.string(), + UnsubscribeUrl: z.string().url(), + UnsubscribeURL: z.string().url().optional(), + SigningCertUrl: z.string().url().optional(), + SigningCertURL: z.string().url().optional(), + Type: z.literal('Notification'), + MessageAttributes: z.record(z.string(), SnsMsgAttribute).optional(), + Message: z.string(), + MessageId: z.string(), + Signature: z.string().optional(), + SignatureVersion: z.string().optional(), + Timestamp: z.string().datetime(), +}); + +const SnsSqsNotificationSchema = SnsNotificationSchema.extend({ + UnsubscribeURL: z.string().optional(), + SigningCertURL: z.string().url().optional(), +}).omit({ + UnsubscribeUrl: true, + SigningCertUrl: true, +}); + +const SnsRecordSchema = z.object({ + EventSource: z.literal('aws:sns'), + EventVersion: z.string(), + EventSubscriptionArn: z.string(), + Sns: SnsNotificationSchema, +}); + +const SnsSchema = z.object({ + Records: z.array(SnsRecordSchema), +}); + +export { + SnsSchema, + SnsRecordSchema, + SnsNotificationSchema, + SnsMsgAttribute, + SnsSqsNotificationSchema, +}; diff --git a/packages/parser/src/schemas/sqs.ts b/packages/parser/src/schemas/sqs.ts new file mode 100644 index 0000000000..5484246e0d --- /dev/null +++ b/packages/parser/src/schemas/sqs.ts @@ -0,0 +1,48 @@ +import { z } from 'zod'; + +const SqsMsgAttributeSchema = z.object({ + stringValue: z.string().optional(), + binaryValue: z.string().optional(), + stringListValues: z.array(z.string()).optional(), + binaryListValues: z.array(z.string()).optional(), + dataType: z.string(), +}); + +const SqsAttributesSchema = z.object({ + ApproximateReceiveCount: z.string(), + ApproximateFirstReceiveTimestamp: z.string(), + MessageDeduplicationId: z.string().optional(), + MessageGroupId: z.string().optional(), + SenderId: z.string(), + SentTimestamp: z.string(), + SequenceNumber: z.string().optional(), + AWSTraceHeader: z.string().optional(), + /** + * Undocumented, but used by AWS to support their re-drive functionality in the console + */ + DeadLetterQueueSourceArn: z.string().optional(), +}); + +const SqsRecordSchema = z.object({ + messageId: z.string(), + receiptHandle: z.string(), + body: z.string(), + attributes: SqsAttributesSchema, + messageAttributes: z.record(z.string(), SqsMsgAttributeSchema), + md5OfBody: z.string(), + md5OfMessageAttributes: z.string().optional().nullable(), + eventSource: z.literal('aws:sqs'), + eventSourceARN: z.string(), + awsRegion: z.string(), +}); + +const SqsSchema = z.object({ + Records: z.array(SqsRecordSchema), +}); + +export { + SqsSchema, + SqsRecordSchema, + SqsAttributesSchema, + SqsMsgAttributeSchema, +}; diff --git a/packages/parser/src/schemas/vpc-lattice.ts b/packages/parser/src/schemas/vpc-lattice.ts new file mode 100644 index 0000000000..f8f266fa20 --- /dev/null +++ b/packages/parser/src/schemas/vpc-lattice.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +const VpcLatticeSchema = z.object({ + method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']), + raw_path: z.string(), + body: z.string(), + is_base64_encoded: z.boolean(), + headers: z.record(z.string(), z.string()), + query_string_parameters: z.record(z.string(), z.string()), +}); + +export { VpcLatticeSchema }; diff --git a/packages/parser/src/schemas/vpc-latticev2.ts b/packages/parser/src/schemas/vpc-latticev2.ts new file mode 100644 index 0000000000..1ef3fd671f --- /dev/null +++ b/packages/parser/src/schemas/vpc-latticev2.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; + +const VpcLatticeV2RequestContextIdentity = z.object({ + sourceVpcArn: z.string().optional(), + type: z.string().optional(), + principal: z.string().optional(), + principalOrgId: z.string().optional(), + sessionName: z.string().optional(), + X509SubjectCn: z.string().optional(), + X509IssuerOu: z.string().optional(), + x509SanDns: z.string().optional(), + x509SanUri: z.string().optional(), + X509SanNameCn: z.string().optional(), +}); + +const VpcLatticeV2RequestContext = z.object({ + serviceNetworkArn: z.string(), + serviceArn: z.string(), + targetGroupArn: z.string(), + region: z.string(), + timeEpoch: z.string(), + identity: VpcLatticeV2RequestContextIdentity, +}); + +const VpcLatticeV2Schema = z.object({ + version: z.string(), + path: z.string(), + method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']), + headers: z.record(z.string(), z.string()), + queryStringParameters: z.record(z.string(), z.string()).optional(), + body: z.string().optional(), + isBase64Encoded: z.boolean().optional(), + requestContext: VpcLatticeV2RequestContext, +}); + +export { VpcLatticeV2Schema }; diff --git a/packages/parser/src/types/envelope.ts b/packages/parser/src/types/envelope.ts new file mode 100644 index 0000000000..9e049c1178 --- /dev/null +++ b/packages/parser/src/types/envelope.ts @@ -0,0 +1,32 @@ +import type { + ApiGatewayEnvelope, + KinesisFirehoseEnvelope, + KinesisEnvelope, + KafkaEnvelope, + CloudWatchEnvelope, + EventBridgeEnvelope, + ApiGatewayV2Envelope, + DynamoDBStreamEnvelope, + LambdaFunctionUrlEnvelope, + SnsEnvelope, + SnsSqsEnvelope, + SqsEnvelope, + VpcLatticeEnvelope, + VpcLatticeV2Envelope, +} from '../envelopes/index.js'; + +export type Envelope = + | typeof ApiGatewayEnvelope + | typeof ApiGatewayV2Envelope + | typeof CloudWatchEnvelope + | typeof DynamoDBStreamEnvelope + | typeof EventBridgeEnvelope + | typeof KafkaEnvelope + | typeof KinesisEnvelope + | typeof KinesisFirehoseEnvelope + | typeof LambdaFunctionUrlEnvelope + | typeof SnsEnvelope + | typeof SnsSqsEnvelope + | typeof SqsEnvelope + | typeof VpcLatticeEnvelope + | typeof VpcLatticeV2Envelope; diff --git a/packages/parser/src/types/index.ts b/packages/parser/src/types/index.ts new file mode 100644 index 0000000000..26052e60b6 --- /dev/null +++ b/packages/parser/src/types/index.ts @@ -0,0 +1,36 @@ +export type { + ParserOptions, + ParsedResult, + ParsedResultSuccess, + ParsedResultError, +} from '../types/parser.js'; +export type { Envelope } from './envelope.js'; + +export type { + ALBEvent, + APIGatewayProxyEvent, + ALBMultiValueHeadersEvent, + APIGatewayProxyEventV2, + S3Event, + S3EventNotificationEventBridge, + S3SqsEventNotification, + SnsEvent, + SqsEvent, + DynamoDBStreamEvent, + CloudWatchLogsEvent, + CloudFormationCustomResourceCreateEvent, + CloudFormationCustomResourceDeleteEvent, + CloudFormationCustomResourceUpdateEvent, + EventBridgeEvent, + KafkaSelfManagedEvent, + KafkaMskEvent, + KinesisDataStreamEvent, + KinesisDataStreamRecord, + KinesisDataStreamRecordPayload, + KinesisFireHoseEvent, + KinesisFireHoseSqsEvent, + LambdaFunctionUrlEvent, + SesEvent, + VpcLatticeEvent, + VpcLatticeEventV2, +} from './schema.js'; diff --git a/packages/parser/src/types/parser.ts b/packages/parser/src/types/parser.ts new file mode 100644 index 0000000000..c775a78d94 --- /dev/null +++ b/packages/parser/src/types/parser.ts @@ -0,0 +1,30 @@ +import type { ZodSchema, ZodError } from 'zod'; +import type { Envelope } from './envelope.js'; + +type ParserOptions = { + schema: S; + envelope?: Envelope; + safeParse?: boolean; +}; + +type ParsedResultSuccess = { + success: true; + data: Output; +}; + +type ParsedResultError = { + success: false; + error: ZodError | Error; + originalEvent: Input; +}; + +type ParsedResult = + | ParsedResultSuccess + | ParsedResultError; + +export type { + ParserOptions, + ParsedResult, + ParsedResultError, + ParsedResultSuccess, +}; diff --git a/packages/parser/src/types/schema.ts b/packages/parser/src/types/schema.ts new file mode 100644 index 0000000000..6b18b57968 --- /dev/null +++ b/packages/parser/src/types/schema.ts @@ -0,0 +1,120 @@ +import { + KafkaSelfManagedEventSchema, + KafkaMskEventSchema, +} from '../schemas/kafka.js'; +import { z } from 'zod'; +import { + KinesisDataStreamRecord, + KinesisDataStreamRecordPayload, + KinesisDataStreamSchema, +} from '../schemas/kinesis.js'; +import { APIGatewayProxyEventSchema } from '../schemas/apigw.js'; +import { AlbSchema, AlbMultiValueHeadersSchema } from '../schemas/alb.js'; +import { APIGatewayProxyEventV2Schema } from '../schemas/apigwv2.js'; +import { DynamoDBStreamSchema } from '../schemas/dynamodb.js'; +import { SqsSchema } from '../schemas/sqs.js'; +import { + CloudFormationCustomResourceCreateSchema, + CloudFormationCustomResourceDeleteSchema, + CloudFormationCustomResourceUpdateSchema, +} from '../schemas/cloudformation-custom-resource.js'; +import { CloudWatchLogsSchema } from '../schemas/cloudwatch.js'; +import { EventBridgeSchema } from '../schemas/eventbridge.js'; +import { + KinesisFirehoseSchema, + KinesisFirehoseSqsSchema, +} from '../schemas/kinesis-firehose.js'; +import { LambdaFunctionUrlSchema } from '../schemas/lambda.js'; +import { + S3EventNotificationEventBridgeSchema, + S3Schema, + S3SqsEventNotificationSchema, +} from '../schemas/s3.js'; +import { SesSchema } from '../schemas/ses.js'; +import { SnsSchema } from '../schemas/sns.js'; +import { VpcLatticeSchema } from '../schemas/vpc-lattice.js'; +import { VpcLatticeV2Schema } from '../schemas/vpc-latticev2.js'; + +type ALBEvent = z.infer; + +type ALBMultiValueHeadersEvent = z.infer; + +type APIGatewayProxyEvent = z.infer; +type APIGatewayProxyEventV2 = z.infer; + +type CloudFormationCustomResourceCreateEvent = z.infer< + typeof CloudFormationCustomResourceCreateSchema +>; + +type CloudFormationCustomResourceDeleteEvent = z.infer< + typeof CloudFormationCustomResourceDeleteSchema +>; + +type CloudFormationCustomResourceUpdateEvent = z.infer< + typeof CloudFormationCustomResourceUpdateSchema +>; + +type CloudWatchLogsEvent = z.infer; + +type DynamoDBStreamEvent = z.infer; + +type EventBridgeEvent = z.infer; + +type KafkaSelfManagedEvent = z.infer; + +type KafkaMskEvent = z.infer; + +type KinesisDataStreamEvent = z.infer; + +type KinesisFireHoseEvent = z.infer; + +type KinesisFireHoseSqsEvent = z.infer; + +type LambdaFunctionUrlEvent = z.infer; + +type S3Event = z.infer; + +type S3EventNotificationEventBridge = z.infer< + typeof S3EventNotificationEventBridgeSchema +>; + +type S3SqsEventNotification = z.infer; + +type SesEvent = z.infer; + +type SnsEvent = z.infer; + +type SqsEvent = z.infer; + +type VpcLatticeEvent = z.infer; + +type VpcLatticeEventV2 = z.infer; + +export type { + ALBEvent, + ALBMultiValueHeadersEvent, + APIGatewayProxyEvent, + APIGatewayProxyEventV2, + CloudFormationCustomResourceCreateEvent, + CloudFormationCustomResourceDeleteEvent, + CloudFormationCustomResourceUpdateEvent, + CloudWatchLogsEvent, + DynamoDBStreamEvent, + EventBridgeEvent, + KafkaSelfManagedEvent, + KafkaMskEvent, + KinesisDataStreamEvent, + KinesisDataStreamRecord, + KinesisDataStreamRecordPayload, + KinesisFireHoseEvent, + KinesisFireHoseSqsEvent, + LambdaFunctionUrlEvent, + S3Event, + S3EventNotificationEventBridge, + S3SqsEventNotification, + SesEvent, + SnsEvent, + SqsEvent, + VpcLatticeEvent, + VpcLatticeEventV2, +}; diff --git a/packages/parser/tests/events/activeMQEvent.json b/packages/parser/tests/events/activeMQEvent.json new file mode 100644 index 0000000000..57417cfd22 --- /dev/null +++ b/packages/parser/tests/events/activeMQEvent.json @@ -0,0 +1,55 @@ +{ + "eventSource": "aws:amq", + "eventSourceArn": "arn:aws:mq:us-west-2:112556298976:broker:test:b-9bcfa592-423a-4942-879d-eb284b418fc8", + "messages": [ + { + "messageID": "ID:b-9bcfa592-423a-4942-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1", + "messageType": "jms/text-message", + "data": "QUJDOkFBQUE=", + "connectionId": "myJMSCoID", + "redelivered": false, + "destination": { + "physicalname": "testQueue" + }, + "timestamp": 1598827811958, + "brokerInTime": 1598827811958, + "brokerOutTime": 1598827811959, + "properties": { + "testKey": "testValue" + } + }, + { + "messageID": "ID:b-9bcfa592-423a-4942-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1", + "messageType": "jms/text-message", + "data": "eyJ0aW1lb3V0IjowLCJkYXRhIjoiQ1pybWYwR3c4T3Y0YnFMUXhENEUifQ==", + "connectionId": "myJMSCoID2", + "redelivered": false, + "destination": { + "physicalname": "testQueue" + }, + "timestamp": 1598827811958, + "brokerInTime": 1598827811958, + "brokerOutTime": 1598827811959, + "properties": { + "testKey": "testValue" + } + + }, + { + "messageID": "ID:b-9bcfa592-423a-4942-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1", + "messageType": "jms/bytes-message", + "data": "3DTOOW7crj51prgVLQaGQ82S48k=", + "connectionId": "myJMSCoID1", + "persistent": false, + "destination": { + "physicalname": "testQueue" + }, + "timestamp": 1598827811958, + "brokerInTime": 1598827811958, + "brokerOutTime": 1598827811959, + "properties": { + "testKey": "testValue" + } + } + ] +} diff --git a/packages/parser/tests/events/albEvent.json b/packages/parser/tests/events/albEvent.json new file mode 100644 index 0000000000..9328cb39e1 --- /dev/null +++ b/packages/parser/tests/events/albEvent.json @@ -0,0 +1,28 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "GET", + "path": "/lambda", + "queryStringParameters": { + "query": "1234ABCD" + }, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20" + }, + "body": "Test", + "isBase64Encoded": false +} diff --git a/packages/parser/tests/events/albEventPathTrailingSlash.json b/packages/parser/tests/events/albEventPathTrailingSlash.json new file mode 100644 index 0000000000..c517a3f6b0 --- /dev/null +++ b/packages/parser/tests/events/albEventPathTrailingSlash.json @@ -0,0 +1,28 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "GET", + "path": "/lambda/", + "queryStringParameters": { + "query": "1234ABCD" + }, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20" + }, + "body": "Test", + "isBase64Encoded": false + } \ No newline at end of file diff --git a/packages/parser/tests/events/albMultiValueHeadersEvent.json b/packages/parser/tests/events/albMultiValueHeadersEvent.json new file mode 100644 index 0000000000..6b34709605 --- /dev/null +++ b/packages/parser/tests/events/albMultiValueHeadersEvent.json @@ -0,0 +1,35 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:eu-central-1:1234567890:targetgroup/alb-c-Targe-11GDXTPQ7663S/804a67588bfdc10f" + } + }, + "httpMethod": "GET", + "path": "/todos", + "multiValueQueryStringParameters": {}, + "multiValueHeaders": { + "accept": [ + "*/*" + ], + "host": [ + "alb-c-LoadB-14POFKYCLBNSF-1815800096.eu-central-1.elb.amazonaws.com" + ], + "user-agent": [ + "curl/7.79.1" + ], + "x-amzn-trace-id": [ + "Root=1-62fa9327-21cdd4da4c6db451490a5fb7" + ], + "x-forwarded-for": [ + "123.123.123.123" + ], + "x-forwarded-port": [ + "80" + ], + "x-forwarded-proto": [ + "http" + ] + }, + "body": "", + "isBase64Encoded": false +} diff --git a/packages/parser/tests/events/apiGatewayAuthorizerRequestEvent.json b/packages/parser/tests/events/apiGatewayAuthorizerRequestEvent.json new file mode 100644 index 0000000000..6a80fff823 --- /dev/null +++ b/packages/parser/tests/events/apiGatewayAuthorizerRequestEvent.json @@ -0,0 +1,68 @@ +{ + "version": "1.0", + "type": "REQUEST", + "methodArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "identitySource": "user1,123", + "authorizationToken": "user1,123", + "resource": "/request", + "path": "/request", + "httpMethod": "GET", + "headers": { + "X-AMZ-Date": "20170718T062915Z", + "Accept": "*/*", + "HeaderAuth1": "headerValue1", + "CloudFront-Viewer-Country": "US", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Is-Mobile-Viewer": "false", + "User-Agent": "..." + }, + "queryStringParameters": { + "QueryString1": "queryValue1" + }, + "pathParameters": {}, + "stageVariables": { + "StageVar1": "stageValue1" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "abcdef123", + "domainName": "3npb9j1tlk.execute-api.us-west-1.amazonaws.com", + "domainPrefix": "3npb9j1tlk", + "extendedRequestId": "EXqgWgXxSK4EJug=", + "httpMethod": "GET", + "identity": { + "accessKey": null, + "accountId": null, + "caller": null, + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "principalOrgId": null, + "apiKey": "...", + "sourceIp": "192.168.0.1", + "user": null, + "userAgent": "PostmanRuntime/7.28.3", + "userArn": null, + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "path": "/request", + "protocol": "HTTP/1.1", + "requestId": "EXqgWgXxSK4EJug=", + "requestTime": "20/Aug/2021:14:36:50 +0000", + "requestTimeEpoch": 1629470210043, + "resourceId": "ANY /request", + "resourcePath": "/request", + "stage": "test" + } +} diff --git a/packages/parser/tests/events/apiGatewayAuthorizerTokenEvent.json b/packages/parser/tests/events/apiGatewayAuthorizerTokenEvent.json new file mode 100644 index 0000000000..f30f360f6d --- /dev/null +++ b/packages/parser/tests/events/apiGatewayAuthorizerTokenEvent.json @@ -0,0 +1,5 @@ +{ + "type": "TOKEN", + "authorizationToken": "allow", + "methodArn": "arn:aws:execute-api:us-west-2:123456789012:ymy8tbxw7b/*/GET/" +} diff --git a/packages/parser/tests/events/apiGatewayAuthorizerV2Event.json b/packages/parser/tests/events/apiGatewayAuthorizerV2Event.json new file mode 100644 index 0000000000..f0528080c9 --- /dev/null +++ b/packages/parser/tests/events/apiGatewayAuthorizerV2Event.json @@ -0,0 +1,52 @@ +{ + "version": "2.0", + "type": "REQUEST", + "routeArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "identitySource": ["user1", "123"], + "routeKey": "GET /merchants", + "rawPath": "/merchants", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": ["cookie1", "cookie2"], + "headers": { + "x-amzn-trace-id": "Root=1-611cc4a7-0746ebee281cfd967db97b64", + "Header1": "value1", + "Header2": "value2", + "Authorization": "value" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/merchants", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "GET /merchants", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": { "parameter1": "value1" }, + "stageVariables": { "stageVariable1": "value1", "stageVariable2": "value2" } +} diff --git a/packages/parser/tests/events/apiGatewayProxyEvent.json b/packages/parser/tests/events/apiGatewayProxyEvent.json new file mode 100644 index 0000000000..4d2fb62399 --- /dev/null +++ b/packages/parser/tests/events/apiGatewayProxyEvent.json @@ -0,0 +1,81 @@ +{ + "version": "1.0", + "resource": "/my/path", + "path": "/my/path", + "httpMethod": "GET", + "headers": { + "Header1": "value1", + "Header2": "value2", + "Origin": "https://aws.amazon.com" + }, + "multiValueHeaders": { + "Header1": [ + "value1" + ], + "Header2": [ + "value1", + "value2" + ] + }, + "queryStringParameters": { + "parameter1": "value1", + "parameter2": "value" + }, + "multiValueQueryStringParameters": { + "parameter1": [ + "value1", + "value2" + ], + "parameter2": [ + "value" + ] + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "extendedRequestId": "request-id", + "httpMethod": "GET", + "identity": { + "accessKey": null, + "accountId": null, + "caller": null, + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "principalOrgId": null, + "sourceIp": "192.168.0.1", + "user": null, + "userAgent": "user-agent", + "userArn": null, + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "path": "/my/path", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/my/path", + "stage": "$default" + }, + "pathParameters": null, + "stageVariables": null, + "body": "Hello from Lambda!", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/packages/parser/tests/events/apiGatewayProxyEventAnotherPath.json b/packages/parser/tests/events/apiGatewayProxyEventAnotherPath.json new file mode 100644 index 0000000000..660118a2f9 --- /dev/null +++ b/packages/parser/tests/events/apiGatewayProxyEventAnotherPath.json @@ -0,0 +1,80 @@ +{ + "version": "1.0", + "resource": "/my/anotherPath", + "path": "/my/anotherPath", + "httpMethod": "GET", + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "multiValueHeaders": { + "Header1": [ + "value1" + ], + "Header2": [ + "value1", + "value2" + ] + }, + "queryStringParameters": { + "parameter1": "value1", + "parameter2": "value" + }, + "multiValueQueryStringParameters": { + "parameter1": [ + "value1", + "value2" + ], + "parameter2": [ + "value" + ] + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "extendedRequestId": "request-id", + "httpMethod": "GET", + "identity": { + "accessKey": null, + "accountId": null, + "caller": null, + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "principalOrgId": null, + "sourceIp": "192.168.0.1", + "user": null, + "userAgent": "user-agent", + "userArn": null, + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "path": "/my/anotherPath", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/my/anotherPath", + "stage": "$default" + }, + "pathParameters": null, + "stageVariables": null, + "body": "Hello from Lambda!", + "isBase64Encoded": true +} \ No newline at end of file diff --git a/packages/parser/tests/events/apiGatewayProxyEventPathTrailingSlash.json b/packages/parser/tests/events/apiGatewayProxyEventPathTrailingSlash.json new file mode 100644 index 0000000000..ced73da8c9 --- /dev/null +++ b/packages/parser/tests/events/apiGatewayProxyEventPathTrailingSlash.json @@ -0,0 +1,80 @@ +{ + "version": "1.0", + "resource": "/my/path", + "path": "/my/path/", + "httpMethod": "GET", + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "multiValueHeaders": { + "Header1": [ + "value1" + ], + "Header2": [ + "value1", + "value2" + ] + }, + "queryStringParameters": { + "parameter1": "value1", + "parameter2": "value" + }, + "multiValueQueryStringParameters": { + "parameter1": [ + "value1", + "value2" + ], + "parameter2": [ + "value" + ] + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "extendedRequestId": "request-id", + "httpMethod": "GET", + "identity": { + "accessKey": null, + "accountId": null, + "caller": null, + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "principalOrgId": null, + "sourceIp": "192.168.0.1", + "user": null, + "userAgent": "user-agent", + "userArn": null, + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "path": "/my/path", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/my/path", + "stage": "$default" + }, + "pathParameters": null, + "stageVariables": null, + "body": "Hello from Lambda!", + "isBase64Encoded": true + } \ No newline at end of file diff --git a/packages/parser/tests/events/apiGatewayProxyEventPrincipalId.json b/packages/parser/tests/events/apiGatewayProxyEventPrincipalId.json new file mode 100644 index 0000000000..f18a2a44bb --- /dev/null +++ b/packages/parser/tests/events/apiGatewayProxyEventPrincipalId.json @@ -0,0 +1,13 @@ +{ + "resource": "/trip", + "path": "/trip", + "httpMethod": "POST", + "requestContext": { + "requestId": "34972478-2843-4ced-a657-253108738274", + "authorizer": { + "user_id": "fake_username", + "principalId": "fake", + "integrationLatency": 451 + } + } +} diff --git a/packages/parser/tests/events/apiGatewayProxyEvent_noVersionAuth.json b/packages/parser/tests/events/apiGatewayProxyEvent_noVersionAuth.json new file mode 100644 index 0000000000..3a4af1ae9a --- /dev/null +++ b/packages/parser/tests/events/apiGatewayProxyEvent_noVersionAuth.json @@ -0,0 +1,75 @@ +{ + "resource": "/my/path", + "path": "/my/path", + "httpMethod": "GET", + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "multiValueHeaders": { + "Header1": [ + "value1" + ], + "Header2": [ + "value1", + "value2" + ] + }, + "queryStringParameters": { + "parameter1": "value1", + "parameter2": "value" + }, + "multiValueQueryStringParameters": { + "parameter1": [ + "value1", + "value2" + ], + "parameter2": [ + "value" + ] + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "extendedRequestId": "request-id", + "httpMethod": "GET", + "identity": { + "accessKey": null, + "accountId": null, + "caller": null, + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "principalOrgId": null, + "sourceIp": "192.168.0.1", + "user": null, + "userAgent": "user-agent", + "userArn": null, + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "path": "/my/path", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/my/path", + "stage": "$default" + }, + "pathParameters": null, + "stageVariables": null, + "body": "Hello from Lambda!", + "isBase64Encoded": true +} diff --git a/packages/parser/tests/events/apiGatewayProxyOtherEvent.json b/packages/parser/tests/events/apiGatewayProxyOtherEvent.json new file mode 100644 index 0000000000..7d5f0ef753 --- /dev/null +++ b/packages/parser/tests/events/apiGatewayProxyOtherEvent.json @@ -0,0 +1,81 @@ +{ + "version": "1.0", + "resource": "/other/path", + "path": "/other/path", + "httpMethod": "GET", + "headers": { + "Header1": "value1", + "Header2": "value2", + "Origin": "https://aws.amazon.com" + }, + "multiValueHeaders": { + "Header1": [ + "value1" + ], + "Header2": [ + "value1", + "value2" + ] + }, + "queryStringParameters": { + "parameter1": "value1", + "parameter2": "value" + }, + "multiValueQueryStringParameters": { + "parameter1": [ + "value1", + "value2" + ], + "parameter2": [ + "value" + ] + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "extendedRequestId": "request-id", + "httpMethod": "GET", + "identity": { + "accessKey": null, + "accountId": null, + "caller": null, + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "principalOrgId": null, + "sourceIp": "192.168.0.1", + "user": null, + "userAgent": "user-agent", + "userArn": null, + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "path": "/other/path", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/other/path", + "stage": "$default" + }, + "pathParameters": null, + "stageVariables": null, + "body": "Hello from Lambda!", + "isBase64Encoded": false +} diff --git a/packages/parser/tests/events/apiGatewayProxyV2Event.json b/packages/parser/tests/events/apiGatewayProxyV2Event.json new file mode 100644 index 0000000000..ac287d560a --- /dev/null +++ b/packages/parser/tests/events/apiGatewayProxyV2Event.json @@ -0,0 +1,69 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "authorizer": { + "jwt": { + "claims": { + "claim1": "value1", + "claim2": "value2" + }, + "scopes": [ + "scope1", + "scope2" + ] + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "192.168.0.1", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "{\"message\": \"hello world\", \"username\": \"tom\"}", + "pathParameters": { + "parameter1": "value1" + }, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} diff --git a/packages/parser/tests/events/apiGatewayProxyV2EventPathTrailingSlash.json b/packages/parser/tests/events/apiGatewayProxyV2EventPathTrailingSlash.json new file mode 100644 index 0000000000..6a745937ca --- /dev/null +++ b/packages/parser/tests/events/apiGatewayProxyV2EventPathTrailingSlash.json @@ -0,0 +1,69 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path/", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "authorizer": { + "jwt": { + "claims": { + "claim1": "value1", + "claim2": "value2" + }, + "scopes": [ + "scope1", + "scope2" + ] + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "192.168.0.1", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "{\"message\": \"hello world\", \"username\": \"tom\"}", + "pathParameters": { + "parameter1": "value1" + }, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } + } \ No newline at end of file diff --git a/packages/parser/tests/events/apiGatewayProxyV2Event_GET.json b/packages/parser/tests/events/apiGatewayProxyV2Event_GET.json new file mode 100644 index 0000000000..34b9b04628 --- /dev/null +++ b/packages/parser/tests/events/apiGatewayProxyV2Event_GET.json @@ -0,0 +1,68 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "authorizer": { + "jwt": { + "claims": { + "claim1": "value1", + "claim2": "value2" + }, + "scopes": [ + "scope1", + "scope2" + ] + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "GET", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "192.168.0.1", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": { + "parameter1": "value1" + }, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} diff --git a/packages/parser/tests/events/apiGatewayProxyV2IamEvent.json b/packages/parser/tests/events/apiGatewayProxyV2IamEvent.json new file mode 100644 index 0000000000..99f10ef989 --- /dev/null +++ b/packages/parser/tests/events/apiGatewayProxyV2IamEvent.json @@ -0,0 +1,62 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "pathParameters": { + "proxy": "hello/world" + }, + "requestContext": { + "routeKey": "$default", + "accountId": "123456789012", + "stage": "$default", + "requestId": "id", + "authorizer": { + "iam": { + "accessKey": "ARIA2ZJZYVUEREEIHAKY", + "accountId": "1234567890", + "callerId": "AROA7ZJZYVRE7C3DUXHH6:CognitoIdentityCredentials", + "cognitoIdentity": { + "amr": [ + "foo" + ], + "identityId": "us-east-1:3f291106-8703-466b-8f2b-3ecee1ca56ce", + "identityPoolId": "us-east-1:4f291106-8703-466b-8f2b-3ecee1ca56ce" + }, + "principalOrgId": "AwsOrgId", + "userArn": "arn:aws:iam::1234567890:user/Admin", + "userId": "AROA2ZJZYVRE7Y3TUXHH6" + } + }, + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "time": "12/Mar/2020:19:03:58+0000", + "timeEpoch": 1583348638390, + "http": { + "method": "GET", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "192.168.0.1", + "userAgent": "agent" + } + }, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + }, + "body": "{\r\n\t\"a\": 1\r\n}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/packages/parser/tests/events/apiGatewayProxyV2LambdaAuthorizerEvent.json b/packages/parser/tests/events/apiGatewayProxyV2LambdaAuthorizerEvent.json new file mode 100644 index 0000000000..e7e6aeb2ca --- /dev/null +++ b/packages/parser/tests/events/apiGatewayProxyV2LambdaAuthorizerEvent.json @@ -0,0 +1,50 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "pathParameters": { + "proxy": "hello/world" + }, + "requestContext": { + "routeKey": "$default", + "accountId": "123456789012", + "stage": "$default", + "requestId": "id", + "authorizer": { + "lambda": { + "key": "value" + } + }, + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "time": "12/Mar/2020:19:03:58+0000", + "timeEpoch": 1583348638390, + "http": { + "method": "GET", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "192.168.0.1", + "userAgent": "agent" + } + }, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + }, + "body": "{\r\n\t\"a\": 1\r\n}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/packages/parser/tests/events/apiGatewayProxyV2OtherGetEvent.json b/packages/parser/tests/events/apiGatewayProxyV2OtherGetEvent.json new file mode 100644 index 0000000000..c5499c46b4 --- /dev/null +++ b/packages/parser/tests/events/apiGatewayProxyV2OtherGetEvent.json @@ -0,0 +1,68 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/other/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "authorizer": { + "jwt": { + "claims": { + "claim1": "value1", + "claim2": "value2" + }, + "scopes": [ + "scope1", + "scope2" + ] + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "GET", + "path": "/other/path", + "protocol": "HTTP/1.1", + "sourceIp": "192.168.0.1", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": { + "parameter1": "value1" + }, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} diff --git a/packages/parser/tests/events/apiGatewayProxyV2SchemaMiddlewareInvalidEvent.json b/packages/parser/tests/events/apiGatewayProxyV2SchemaMiddlewareInvalidEvent.json new file mode 100644 index 0000000000..5b663ec40a --- /dev/null +++ b/packages/parser/tests/events/apiGatewayProxyV2SchemaMiddlewareInvalidEvent.json @@ -0,0 +1,69 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "authorizer": { + "jwt": { + "claims": { + "claim1": "value1", + "claim2": "value2" + }, + "scopes": [ + "scope1", + "scope2" + ] + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "192.168.0.1", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "{\"username\": \"lessa\"}", + "pathParameters": { + "parameter1": "value1" + }, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} diff --git a/packages/parser/tests/events/apiGatewayProxyV2SchemaMiddlewareValidEvent.json b/packages/parser/tests/events/apiGatewayProxyV2SchemaMiddlewareValidEvent.json new file mode 100644 index 0000000000..f59a6ef318 --- /dev/null +++ b/packages/parser/tests/events/apiGatewayProxyV2SchemaMiddlewareValidEvent.json @@ -0,0 +1,69 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "authorizer": { + "jwt": { + "claims": { + "claim1": "value1", + "claim2": "value2" + }, + "scopes": [ + "scope1", + "scope2" + ] + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "192.168.0.1", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "{\"message\": \"hello world\", \"username\": \"lessa\"}", + "pathParameters": { + "parameter1": "value1" + }, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} diff --git a/packages/parser/tests/events/apiGatewaySchemaMiddlewareInvalidEvent.json b/packages/parser/tests/events/apiGatewaySchemaMiddlewareInvalidEvent.json new file mode 100644 index 0000000000..f601583a76 --- /dev/null +++ b/packages/parser/tests/events/apiGatewaySchemaMiddlewareInvalidEvent.json @@ -0,0 +1,81 @@ +{ + "version": "1.0", + "resource": "/my/path", + "path": "/my/path", + "httpMethod": "POST", + "headers": { + "Header1": "value1", + "Header2": "value2", + "Origin": "https://aws.amazon.com" + }, + "multiValueHeaders": { + "Header1": [ + "value1" + ], + "Header2": [ + "value1", + "value2" + ] + }, + "queryStringParameters": { + "parameter1": "value1", + "parameter2": "value" + }, + "multiValueQueryStringParameters": { + "parameter1": [ + "value1", + "value2" + ], + "parameter2": [ + "value" + ] + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "extendedRequestId": "request-id", + "httpMethod": "GET", + "identity": { + "accessKey": null, + "accountId": null, + "caller": null, + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "principalOrgId": null, + "sourceIp": "192.168.0.1", + "user": null, + "userAgent": "user-agent", + "userArn": null, + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "path": "/my/path", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/my/path", + "stage": "$default" + }, + "pathParameters": null, + "stageVariables": null, + "body": "{\"username\": \"lessa\"}", + "isBase64Encoded": false +} diff --git a/packages/parser/tests/events/apiGatewaySchemaMiddlewareValidEvent.json b/packages/parser/tests/events/apiGatewaySchemaMiddlewareValidEvent.json new file mode 100644 index 0000000000..7437eba9e0 --- /dev/null +++ b/packages/parser/tests/events/apiGatewaySchemaMiddlewareValidEvent.json @@ -0,0 +1,81 @@ +{ + "version": "1.0", + "resource": "/my/path", + "path": "/my/path", + "httpMethod": "POST", + "headers": { + "Header1": "value1", + "Header2": "value2", + "Origin": "https://aws.amazon.com" + }, + "multiValueHeaders": { + "Header1": [ + "value1" + ], + "Header2": [ + "value1", + "value2" + ] + }, + "queryStringParameters": { + "parameter1": "value1", + "parameter2": "value" + }, + "multiValueQueryStringParameters": { + "parameter1": [ + "value1", + "value2" + ], + "parameter2": [ + "value" + ] + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "extendedRequestId": "request-id", + "httpMethod": "GET", + "identity": { + "accessKey": null, + "accountId": null, + "caller": null, + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "principalOrgId": null, + "sourceIp": "192.168.0.1", + "user": null, + "userAgent": "user-agent", + "userArn": null, + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "path": "/my/path", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/my/path", + "stage": "$default" + }, + "pathParameters": null, + "stageVariables": null, + "body": "{\"message\": \"hello world\", \"username\": \"lessa\"}", + "isBase64Encoded": false +} diff --git a/packages/parser/tests/events/appSyncAuthorizerEvent.json b/packages/parser/tests/events/appSyncAuthorizerEvent.json new file mode 100644 index 0000000000..a8264569bf --- /dev/null +++ b/packages/parser/tests/events/appSyncAuthorizerEvent.json @@ -0,0 +1,13 @@ +{ + "authorizationToken": "BE9DC5E3-D410-4733-AF76-70178092E681", + "requestContext": { + "apiId": "giy7kumfmvcqvbedntjwjvagii", + "accountId": "254688921111", + "requestId": "b80ed838-14c6-4500-b4c3-b694c7bef086", + "queryString": "mutation MyNewTask($desc: String!) {\n createTask(description: $desc, owner: \"ccc\", taskStatus: \"cc\", title: \"ccc\") {\n id\n }\n}\n", + "operationName": "MyNewTask", + "variables": { + "desc": "Foo" + } + } +} diff --git a/packages/parser/tests/events/appSyncAuthorizerResponse.json b/packages/parser/tests/events/appSyncAuthorizerResponse.json new file mode 100644 index 0000000000..7dd8234d2e --- /dev/null +++ b/packages/parser/tests/events/appSyncAuthorizerResponse.json @@ -0,0 +1,9 @@ +{ + "isAuthorized": true, + "resolverContext": { + "name": "Foo Man", + "balance": 100 + }, + "deniedFields": ["Mutation.createEvent"], + "ttlOverride": 15 +} diff --git a/packages/parser/tests/events/appSyncDirectResolver.json b/packages/parser/tests/events/appSyncDirectResolver.json new file mode 100644 index 0000000000..08c3d00b20 --- /dev/null +++ b/packages/parser/tests/events/appSyncDirectResolver.json @@ -0,0 +1,74 @@ +{ + "arguments": { + "id": "my identifier" + }, + "identity": { + "claims": { + "sub": "192879fc-a240-4bf1-ab5a-d6a00f3063f9", + "email_verified": true, + "iss": "https://cognito-idp.us-west-2.amazonaws.com/us-west-xxxxxxxxxxx", + "phone_number_verified": false, + "cognito:username": "jdoe", + "aud": "7471s60os7h0uu77i1tk27sp9n", + "event_id": "bc334ed8-a938-4474-b644-9547e304e606", + "token_use": "id", + "auth_time": 1599154213, + "phone_number": "+19999999999", + "exp": 1599157813, + "iat": 1599154213, + "email": "jdoe@email.com" + }, + "defaultAuthStrategy": "ALLOW", + "groups": null, + "issuer": "https://cognito-idp.us-west-2.amazonaws.com/us-west-xxxxxxxxxxx", + "sourceIp": [ + "1.1.1.1" + ], + "sub": "192879fc-a240-4bf1-ab5a-d6a00f3063f9", + "username": "jdoe" + }, + "source": null, + "request": { + "headers": { + "x-forwarded-for": "1.1.1.1, 2.2.2.2", + "cloudfront-viewer-country": "US", + "cloudfront-is-tablet-viewer": "false", + "via": "2.0 xxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)", + "cloudfront-forwarded-proto": "https", + "origin": "https://us-west-1.console.aws.amazon.com", + "content-length": "217", + "accept-language": "en-US,en;q=0.9", + "host": "xxxxxxxxxxxxxxxx.appsync-api.us-west-1.amazonaws.com", + "x-forwarded-proto": "https", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36", + "accept": "*/*", + "cloudfront-is-mobile-viewer": "false", + "cloudfront-is-smarttv-viewer": "false", + "accept-encoding": "gzip, deflate, br", + "referer": "https://us-west-1.console.aws.amazon.com/appsync/home?region=us-west-1", + "content-type": "application/json", + "sec-fetch-mode": "cors", + "x-amz-cf-id": "3aykhqlUwQeANU-HGY7E_guV5EkNeMMtwyOgiA==", + "x-amzn-trace-id": "Root=1-5f512f51-fac632066c5e848ae714", + "authorization": "eyJraWQiOiJScWFCSlJqYVJlM0hrSnBTUFpIcVRXazNOW...", + "sec-fetch-dest": "empty", + "x-amz-user-agent": "AWS-Console-AppSync/", + "cloudfront-is-desktop-viewer": "true", + "sec-fetch-site": "cross-site", + "x-forwarded-port": "443" + } + }, + "prev": null, + "info": { + "selectionSetList": [ + "id", + "field1", + "field2" + ], + "selectionSetGraphQL": "{\n id\n field1\n field2\n}", + "parentTypeName": "Mutation", + "fieldName": "createSomething", + "variables": {} + }, + "stash": {} +} diff --git a/packages/parser/tests/events/appSyncResolverEvent.json b/packages/parser/tests/events/appSyncResolverEvent.json new file mode 100644 index 0000000000..84ac71951c --- /dev/null +++ b/packages/parser/tests/events/appSyncResolverEvent.json @@ -0,0 +1,71 @@ +{ + "typeName": "Merchant", + "fieldName": "locations", + "arguments": { + "page": 2, + "size": 1, + "name": "value" + }, + "identity": { + "claims": { + "sub": "07920713-4526-4642-9c88-2953512de441", + "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_POOL_ID", + "aud": "58rc9bf5kkti90ctmvioppukm9", + "event_id": "7f4c9383-abf6-48b7-b821-91643968b755", + "token_use": "id", + "auth_time": 1615366261, + "name": "Michael Brewer", + "exp": 1615369861, + "iat": 1615366261 + }, + "defaultAuthStrategy": "ALLOW", + "groups": null, + "issuer": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_POOL_ID", + "sourceIp": [ + "11.215.2.22" + ], + "sub": "07920713-4526-4642-9c88-2953512de441", + "username": "mike" + }, + "source": { + "name": "Value", + "nested": { + "name": "value", + "list": [] + } + }, + "request": { + "headers": { + "x-forwarded-for": "11.215.2.22, 64.44.173.11", + "cloudfront-viewer-country": "US", + "cloudfront-is-tablet-viewer": "false", + "via": "2.0 SOMETHING.cloudfront.net (CloudFront)", + "cloudfront-forwarded-proto": "https", + "origin": "https://console.aws.amazon.com", + "content-length": "156", + "accept-language": "en-US,en;q=0.9", + "host": "SOMETHING.appsync-api.us-east-1.amazonaws.com", + "x-forwarded-proto": "https", + "sec-gpc": "1", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) etc.", + "accept": "*/*", + "cloudfront-is-mobile-viewer": "false", + "cloudfront-is-smarttv-viewer": "false", + "accept-encoding": "gzip, deflate, br", + "referer": "https://console.aws.amazon.com/", + "content-type": "application/json", + "sec-fetch-mode": "cors", + "x-amz-cf-id": "Fo5VIuvP6V6anIEt62WzFDCK45mzM4yEdpt5BYxOl9OFqafd-WR0cA==", + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "authorization": "AUTH-HEADER", + "sec-fetch-dest": "empty", + "x-amz-user-agent": "AWS-Console-AppSync/", + "cloudfront-is-desktop-viewer": "true", + "sec-fetch-site": "cross-site", + "x-forwarded-port": "443" + } + }, + "prev": { + "result": {} + } +} diff --git a/packages/parser/tests/events/awsConfigRuleConfigurationChanged.json b/packages/parser/tests/events/awsConfigRuleConfigurationChanged.json new file mode 100644 index 0000000000..cbf7abf67a --- /dev/null +++ b/packages/parser/tests/events/awsConfigRuleConfigurationChanged.json @@ -0,0 +1,13 @@ +{ + "version":"1.0", + "invokingEvent":"{\"configurationItemDiff\":{\"changedProperties\":{\"Configuration.InstanceType\":{\"previousValue\":\"t2.micro\",\"updatedValue\":\"t2.medium\",\"changeType\":\"UPDATE\"},\"Configuration.State.Name\":{\"previousValue\":\"running\",\"updatedValue\":\"stopped\",\"changeType\":\"UPDATE\"},\"Configuration.StateTransitionReason\":{\"previousValue\":\"\",\"updatedValue\":\"User initiated (2023-04-27 15:01:07 GMT)\",\"changeType\":\"UPDATE\"},\"Configuration.StateReason\":{\"previousValue\":null,\"updatedValue\":{\"code\":\"Client.UserInitiatedShutdown\",\"message\":\"Client.UserInitiatedShutdown: User initiated shutdown\"},\"changeType\":\"CREATE\"},\"Configuration.CpuOptions.CoreCount\":{\"previousValue\":1,\"updatedValue\":2,\"changeType\":\"UPDATE\"}},\"changeType\":\"UPDATE\"},\"configurationItem\":{\"relatedEvents\":[],\"relationships\":[{\"resourceId\":\"eipalloc-0ebb4367662263cc1\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::EIP\",\"name\":\"Is attached to ElasticIp\"},{\"resourceId\":\"eni-034dd31c4b17ada8c\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::NetworkInterface\",\"name\":\"Contains NetworkInterface\"},{\"resourceId\":\"eni-09a604c0ec356b06f\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::NetworkInterface\",\"name\":\"Contains NetworkInterface\"},{\"resourceId\":\"sg-0fb295a327d9b4835\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::SecurityGroup\",\"name\":\"Is associated with SecurityGroup\"},{\"resourceId\":\"subnet-cad1f2f4\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::Subnet\",\"name\":\"Is contained in Subnet\"},{\"resourceId\":\"vol-0a288b5eb9fea4b30\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::Volume\",\"name\":\"Is attached to Volume\"},{\"resourceId\":\"vpc-2d96be57\",\"resourceName\":null,\"resourceType\":\"AWS::EC2::VPC\",\"name\":\"Is contained in Vpc\"}],\"configuration\":{\"amiLaunchIndex\":0,\"imageId\":\"ami-09d95fab7fff3776c\",\"instanceId\":\"i-042dd005362091826\",\"instanceType\":\"t2.medium\",\"kernelId\":null,\"keyName\":\"mihaec2\",\"launchTime\":\"2023-04-27T14:57:16.000Z\",\"monitoring\":{\"state\":\"disabled\"},\"placement\":{\"availabilityZone\":\"us-east-1e\",\"affinity\":null,\"groupName\":\"\",\"partitionNumber\":null,\"hostId\":null,\"tenancy\":\"default\",\"spreadDomain\":null,\"hostResourceGroupArn\":null},\"platform\":null,\"privateDnsName\":\"ip-172-31-78-41.ec2.internal\",\"privateIpAddress\":\"172.31.78.41\",\"productCodes\":[],\"publicDnsName\":\"ec2-3-232-229-57.compute-1.amazonaws.com\",\"publicIpAddress\":\"3.232.229.57\",\"ramdiskId\":null,\"state\":{\"code\":80,\"name\":\"stopped\"},\"stateTransitionReason\":\"User initiated (2023-04-27 15:01:07 GMT)\",\"subnetId\":\"subnet-cad1f2f4\",\"vpcId\":\"vpc-2d96be57\",\"architecture\":\"x86_64\",\"blockDeviceMappings\":[{\"deviceName\":\"/dev/xvda\",\"ebs\":{\"attachTime\":\"2020-05-30T15:21:58.000Z\",\"deleteOnTermination\":true,\"status\":\"attached\",\"volumeId\":\"vol-0a288b5eb9fea4b30\"}}],\"clientToken\":\"\",\"ebsOptimized\":false,\"enaSupport\":true,\"hypervisor\":\"xen\",\"iamInstanceProfile\":{\"arn\":\"arn:aws:iam::0123456789012:instance-profile/AmazonSSMRoleForInstancesQuickSetup\",\"id\":\"AIPAS5S4WFUBL72S3QXW5\"},\"instanceLifecycle\":null,\"elasticGpuAssociations\":[],\"elasticInferenceAcceleratorAssociations\":[],\"networkInterfaces\":[{\"association\":{\"carrierIp\":null,\"ipOwnerId\":\"0123456789012\",\"publicDnsName\":\"ec2-3-232-229-57.compute-1.amazonaws.com\",\"publicIp\":\"3.232.229.57\"},\"attachment\":{\"attachTime\":\"2020-05-30T15:21:57.000Z\",\"attachmentId\":\"eni-attach-0a7e75dc9c1c291a0\",\"deleteOnTermination\":true,\"deviceIndex\":0,\"status\":\"attached\",\"networkCardIndex\":0},\"description\":\"\",\"groups\":[{\"groupName\":\"minhaec2\",\"groupId\":\"sg-0fb295a327d9b4835\"}],\"ipv6Addresses\":[],\"macAddress\":\"06:cf:00:c2:17:db\",\"networkInterfaceId\":\"eni-034dd31c4b17ada8c\",\"ownerId\":\"0123456789012\",\"privateDnsName\":\"ip-172-31-78-41.ec2.internal\",\"privateIpAddress\":\"172.31.78.41\",\"privateIpAddresses\":[{\"association\":{\"carrierIp\":null,\"ipOwnerId\":\"0123456789012\",\"publicDnsName\":\"ec2-3-232-229-57.compute-1.amazonaws.com\",\"publicIp\":\"3.232.229.57\"},\"primary\":true,\"privateDnsName\":\"ip-172-31-78-41.ec2.internal\",\"privateIpAddress\":\"172.31.78.41\"}],\"sourceDestCheck\":true,\"status\":\"in-use\",\"subnetId\":\"subnet-cad1f2f4\",\"vpcId\":\"vpc-2d96be57\",\"interfaceType\":\"interface\"},{\"association\":null,\"attachment\":{\"attachTime\":\"2020-11-26T23:46:04.000Z\",\"attachmentId\":\"eni-attach-0e6d150ebbd19966e\",\"deleteOnTermination\":false,\"deviceIndex\":1,\"status\":\"attached\",\"networkCardIndex\":0},\"description\":\"MINHAEC2AAAAAA\",\"groups\":[{\"groupName\":\"minhaec2\",\"groupId\":\"sg-0fb295a327d9b4835\"},{\"groupName\":\"default\",\"groupId\":\"sg-88105fa0\"}],\"ipv6Addresses\":[],\"macAddress\":\"06:0a:62:00:64:5f\",\"networkInterfaceId\":\"eni-09a604c0ec356b06f\",\"ownerId\":\"0123456789012\",\"privateDnsName\":\"ip-172-31-70-9.ec2.internal\",\"privateIpAddress\":\"172.31.70.9\",\"privateIpAddresses\":[{\"association\":null,\"primary\":true,\"privateDnsName\":\"ip-172-31-70-9.ec2.internal\",\"privateIpAddress\":\"172.31.70.9\"}],\"sourceDestCheck\":true,\"status\":\"in-use\",\"subnetId\":\"subnet-cad1f2f4\",\"vpcId\":\"vpc-2d96be57\",\"interfaceType\":\"interface\"}],\"outpostArn\":null,\"rootDeviceName\":\"/dev/xvda\",\"rootDeviceType\":\"ebs\",\"securityGroups\":[{\"groupName\":\"minhaec2\",\"groupId\":\"sg-0fb295a327d9b4835\"}],\"sourceDestCheck\":true,\"spotInstanceRequestId\":null,\"sriovNetSupport\":null,\"stateReason\":{\"code\":\"Client.UserInitiatedShutdown\",\"message\":\"Client.UserInitiatedShutdown: User initiated shutdown\"},\"tags\":[{\"key\":\"projeto\",\"value\":\"meetup\"},{\"key\":\"Name\",\"value\":\"Minha\"},{\"key\":\"CentroCusto\",\"value\":\"TI\"},{\"key\":\"Setor\",\"value\":\"Desenvolvimento\"}],\"virtualizationType\":\"hvm\",\"cpuOptions\":{\"coreCount\":2,\"threadsPerCore\":1},\"capacityReservationId\":null,\"capacityReservationSpecification\":{\"capacityReservationPreference\":\"open\",\"capacityReservationTarget\":null},\"hibernationOptions\":{\"configured\":false},\"licenses\":[],\"metadataOptions\":{\"state\":\"applied\",\"httpTokens\":\"optional\",\"httpPutResponseHopLimit\":1,\"httpEndpoint\":\"enabled\"},\"enclaveOptions\":{\"enabled\":false},\"bootMode\":null},\"supplementaryConfiguration\":{},\"tags\":{\"projeto\":\"meetup\",\"Setor\":\"Desenvolvimento\",\"CentroCusto\":\"TI\",\"Name\":\"Minha\"},\"configurationItemVersion\":\"1.3\",\"configurationItemCaptureTime\":\"2023-04-27T15:03:11.636Z\",\"configurationStateId\":1682607791636,\"awsAccountId\":\"0123456789012\",\"configurationItemStatus\":\"OK\",\"resourceType\":\"AWS::EC2::Instance\",\"resourceId\":\"i-042dd005362091826\",\"resourceName\":null,\"ARN\":\"arn:aws:ec2:us-east-1:0123456789012:instance/i-042dd005362091826\",\"awsRegion\":\"us-east-1\",\"availabilityZone\":\"us-east-1e\",\"configurationStateMd5Hash\":\"\",\"resourceCreationTime\":\"2023-04-27T14:57:16.000Z\"},\"notificationCreationTime\":\"2023-04-27T15:03:13.332Z\",\"messageType\":\"ConfigurationItemChangeNotification\",\"recordVersion\":\"1.3\"}", + "ruleParameters":"{\"desiredInstanceType\": \"t2.micro\"}", + "resultToken":"eyJlbmNyeXB0ZWREYXRhIjpbLTQxLDEsLTU3LC0zMCwtMTIxLDUzLDUyLDQ1LC01NywtOCw3MywtODEsLTExNiwtMTAyLC01MiwxMTIsLTQ3LDU4LDY1LC0xMjcsMTAyLDUsLTY5LDQ0LC0xNSwxMTQsNDEsLTksMTExLC0zMCw2NSwtNzUsLTM1LDU0LDEwNSwtODksODYsNDAsLTEwNSw5OCw2NSwtMTE5LC02OSwyNCw2NiwtMjAsODAsLTExMiwtNzgsLTgwLDQzLC01NywzMCwtMjUsODIsLTEwLDMsLTQsLTg1LC01MywtMzcsLTkwLC04OCwtOTgsLTk4LC00MSwxOSwxMTYsNjIsLTIzLC0xMjEsLTEwOCw1NywtNTgsLTUyLDI5LDEwMSwxMjIsLTU2LC03MSwtODEsLTQ3LDc3LC0yMiwtMTI0LC0zLC04NiwtMTIyLC00MCwtODksLTEwMSw1NywtMTI3LC0zNywtMzcsLTMxLC05OCwtMzEsMTEsLTEyNSwwLDEwOCwtMzIsNjQsNjIsLTIyLDAsNDcsLTEwNiwtMTAwLDEwNCwxNCw1OCwxMjIsLTEwLC01MCwtOTAsLTgwLC01MCwtNSw2NSwwLC0yNSw4NSw4Miw3LDkzLDEyMiwtODIsLTExNiwtNzksLTQ0LDcyLC03MywtNjksMTQsLTU2LDk0LDkwLDExNCwtMjksLTExOSwtNzEsODgsMTA3LDEwNywxMTAsLTcsMTI3LC0xMjUsLTU3LC0xMjYsLTEyMCw2OSwtMTI3LC03NiwtMTE5LDcxLDEsLTY4LDEwNywxMTMsLTU2LDg3LC0xMDIsLTE2LDEwOCwtMTA3LC00MywtOTQsLTEwNiwzLDkwLDE0LDcyLC0xMiwtMTE2LC03Myw4MCwtMTIyLDQ0LC0xMDQsMTIsNzQsNTcsLTEwLC0xMDUsLTExMiwtMzYsMjgsLTQ1LDk3LDExLC00OSwtMTEsNjEsMzYsLTE3LC03NCw1MCw0LC0yNiwxMDQsLTI4LC0xMjUsMjQsNzAsLTg1LC00Niw5MiwtMTAzLC00MSwtMTA2LDY5LDEyMiwyMSwtMjUsODAsOTksLTkzLC01NiwtMjUsLTQ3LC0xMjMsLTU5LC0xMjQsLTUyLC0xNiwxMjcsLTM4LC0xNiwxMDEsMTE5LDEwNywyNywxMCwtNDYsLTg3LC0xMiwtMzksMTQsNDUsMiw3MCwxMDcsMTA0LC00LC02OSwtMTIsNTksLTEyNiwtOTEsMTI3LDU0LDEwNiwtMTI2LC0xMTYsLTEwMiw3Miw4MSw1MCw3NSwtNTEsMTA4LDQxLC0zLC02LC00NSwxMDMsLTg2LDM3LC00NiwtMzIsLTExMSwxMjQsMTExLDg3LDU0LC03NiwxMjIsLTUsLTM2LC04OCw5LC0xMTMsMTE2LC01OSw4Myw3NywyOCwxMiwtNjUsLTExMywtNzksLTEyOCw4MiwtMTE4LC04MywtMTI0LDMxLDk5LC05MCwtOTksMTYsLTEyMywyMSwtMTE0LC05OCwtMTE2LC0xMTksMiwtNzMsNDYsODIsLTEzLDU0LDcxLC00MiwyNSw3NCw3MywtODYsOTQsNDYsOTksOTMsLTgyLDU1LDY1LC05OCw0OSwtNjAsMTEyLDEwMSwyMiw2OSwtMTYsNzcsLTk0LC01OSwtNDYsMTE1LDMwLC00Myw5Myw4OCwtMjgsMzgsNiw4NCwzMSwtMTAxLDMyLC0yMiwtNjMsLTk1LDExNCwtNzUsMTE0LDM2LC04NCw0MCwtNDQsLTEzLDU5LDcyLC0xLC0xMDMsMzEsMTA1LDY5LDY5LDc3LC02NCwtNTYsMTE4LDEzLC0xMTQsODAsOTksLTUzLDI1LDQyLDk0LDczLC04MCwyNSwzOCwyNCwtMTcsNjYsLTExOCwtMjMsMTE5LDkwLDEyMSwxMTgsLTUxLDUxLC0xMiwtNzYsLTUxLDksLTIxLDExNCwtMzcsLTY0LC0yLC0xMjYsLTk1LDYzLDczLC00MSwtMzQsLTkwLC0yMiw1OSwtNzksMzAsLTQsLTEsLTUsMTIsMzksLTk5LC0xMDUsLTEwNCwtNjEsNjUsLTc0LDE5LC0xMywtNjAsLTI4LC04LDQsLTgsMTIxLC0xMTgsMTIyLC02NSwtMjEsMjMsMTcsLTg0LDQwLC05MiwxNCwtMTI2LC02MCwtNzksLTUzLDM3LC04Myw2NSwxMDQsLTM2LC02MCwtMTEwLC0zMywtMTE3LDYsMTA3LDEsLTMsOTMsNzgsLTk1LC0xMjIsNTMsMTA4LC00OSwtNDksMjQsLTY1LDgzLDEyNSwtNzcsLTE5LC04MSwzNCwtNjcsLTQzLC03MCwtMjYsMTgsMTA0LDY1LDQsLTEyNiw0NCwtMTE5LDUyLC00NiwyMiw2NywxMTMsMTE4LC0zMywzNCwtOTYsMTIxLDE5LC0yLC0zNSwwLC04MiwxNyw2NiwtMjcsNjksLTM2LC0xNCw1NiwtOTcsLTE2LDEyMywyOCwtOTUsLTMyLC02MywtNjksNzAsNjQsLTMzLC0xMDAsNDMsLTExMywxMDUsMTAwLDEwOCwtNjAsNDAsLTIsLTk2LC0xMjQsMzcsLTQ1LC0xMjQsLTY4LC02OSwtMTIzLDE3LC02LDg2LC01OSwtOTQsMTEwLDczLDU3LC0xMTYsMTA3LC00MSwtOTQsLTExOCwtMTI2LDEwLC04MCwtNzAsMTAyLDg4LC0xMjYsODcsLTI3LC0xMDEsLTk0LC0zNSwtMTA2LC02LC03MiwtODYsNTAsMTE2LC0yOCw5MCwxMywtMTIwLDYsMjcsOTIsNTYsLTkwLDM5LDQ5LC0xMywtODYsLTI1LC04NiwxMTMsLTEzLDQxLC0xMTksOTQsLTk0LC0xMDMsLTgzLC02MCwxMjcsLTE1LC0zOSwxMTksLTk1LDI3LDQ0LDExNiwxMDksNywtMTAyLC0xNyw0OCwtODIsLTMxLC04LC02OSwzNSw5NCw1NCwtNTUsMSwtMTE5LDU3LC0xMDgsLTMsLTkxLC0xMjIsLTUzLC04OCw0LC05NywtMzUsMTI2LDExOSw1OSwtMSw4NSw3MywtNTgsLTEyMCwtNjQsMTE5LC0xMTIsOTIsMTksOSwtNjYsLTkyLDEwOCwtMTEsLTQyLDExMSwtMTA0LC0xMjAsMjcsLTEwMywtNjksMTksMTExLDEyLDIzLDEwNyw1NCw0MSwtMjYsNjAsLTMxLC01XSwibWF0ZXJpYWxTZXRTZXJpYWxOdW1iZXIiOjEsIml2UGFyYW1ldGVyU3BlYyI6eyJpdiI6Wy05NSwzMiwxMDgsOTEsMzUsLTgyLC0zNywyNCwtNDQsLTExNSwtODIsLTEyOCwtMTIyLDMsNTMsLTI0XX19", + "eventLeftScope":false, + "executionRoleArn":"arn:aws:iam::0123456789012:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig", + "configRuleArn":"arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "configRuleName":"MyRule", + "configRuleId":"config-rule-i9y8j9", + "accountId":"0123456789012", + "evaluationMode":"DETECTIVE" + } diff --git a/packages/parser/tests/events/awsConfigRuleOversizedConfiguration.json b/packages/parser/tests/events/awsConfigRuleOversizedConfiguration.json new file mode 100644 index 0000000000..5eaef4e001 --- /dev/null +++ b/packages/parser/tests/events/awsConfigRuleOversizedConfiguration.json @@ -0,0 +1,12 @@ +{ + "invokingEvent": "{\"configurationItemSummary\": {\"changeType\": \"UPDATE\",\"configurationItemVersion\": \"1.2\",\"configurationItemCaptureTime\":\"2016-10-06T16:46:16.261Z\",\"configurationStateId\": 0,\"awsAccountId\":\"123456789012\",\"configurationItemStatus\": \"OK\",\"resourceType\": \"AWS::EC2::Instance\",\"resourceId\":\"i-00000000\",\"resourceName\":null,\"ARN\":\"arn:aws:ec2:us-west-2:123456789012:instance/i-00000000\",\"awsRegion\": \"us-west-2\",\"availabilityZone\":\"us-west-2a\",\"configurationStateMd5Hash\":\"8f1ee69b287895a0f8bc5753eca68e96\",\"resourceCreationTime\":\"2016-10-06T16:46:10.489Z\"},\"messageType\":\"OversizedConfigurationItemChangeNotification\", \"notificationCreationTime\": \"2016-10-06T16:46:16.261Z\", \"recordVersion\": \"1.0\"}", + "ruleParameters": "{\"myParameterKey\":\"myParameterValue\"}", + "resultToken": "myResultToken", + "eventLeftScope": false, + "executionRoleArn": "arn:aws:iam::123456789012:role/config-role", + "configRuleArn": "arn:aws:config:us-east-2:123456789012:config-rule/config-rule-ec2-managed-instance-inventory", + "configRuleName": "change-triggered-config-rule", + "configRuleId": "config-rule-0123456", + "accountId": "123456789012", + "version": "1.0" +} diff --git a/packages/parser/tests/events/awsConfigRuleScheduled.json b/packages/parser/tests/events/awsConfigRuleScheduled.json new file mode 100644 index 0000000000..02ce2a0700 --- /dev/null +++ b/packages/parser/tests/events/awsConfigRuleScheduled.json @@ -0,0 +1,13 @@ +{ + "version":"1.0", + "invokingEvent":"{\"awsAccountId\":\"0123456789012\",\"notificationCreationTime\":\"2023-04-27T13:26:17.741Z\",\"messageType\":\"ScheduledNotification\",\"recordVersion\":\"1.0\"}", + "ruleParameters":"{\"test\":\"x\"}", + "resultToken":"eyJlbmNyeXB0ZWREYXRhIjpbLTQyLDEyNiw1MiwtMzcsLTI5LDExNCwxMjYsLTk3LDcxLDIyLC0xMTAsMTEyLC0zMSwtOTMsLTQ5LC0xMDEsODIsMyw1NCw0OSwzLC02OSwtNzEsLTcyLDYyLDgxLC03MiwtODEsNTAsMzUsLTUwLC03NSwtMTE4LC0xMTgsNzcsMTIsLTEsMTQsMTIwLC03MCwxMTAsLTMsNTAsLTYwLDEwNSwtNTcsNDUsMTAyLC0xMDksLTYxLC0xMDEsLTYxLDQsNDcsLTg0LC0yNSwxMTIsNTQsLTcxLC0xMDksNDUsMTksMTIzLC0yNiwxMiwtOTYsLTczLDU0LC0xMDksOTIsNDgsLTU5LC04MywtMzIsODIsLTM2LC05MCwxOSw5OCw3Nyw3OCw0MCw4MCw3OCwtMTA1LDg3LC0xMTMsLTExNiwtNzIsMzAsLTY4LC00MCwtODksMTA5LC0xMDgsLTEyOCwyMiw3Miw3NywtMjEsNzYsODksOTQsLTU5LDgxLC0xMjEsLTEwNywtNjcsNjMsLTcsODIsLTg5LC00NiwtMzQsLTkyLDEyMiwtOTAsMTcsLTEyMywyMCwtODUsLTU5LC03MCw4MSwyNyw2Miw3NCwtODAsODAsMzcsNDAsMTE2LDkxLC0yNCw1MSwtNDEsLTc5LDI4LDEyMCw1MywtMTIyLC04MywxMjYsLTc4LDI1LC05OCwtMzYsMTMsMzIsODYsLTI1LDQ4LDMsLTEwMiwtMTYsMjQsLTMsODUsNDQsLTI4LDE0LDIyLDI3LC0xMjIsMTE4LDEwMSw3Myw1LDE4LDU4LC02NCwyMywtODYsLTExNCwyNCwwLDEwMCwyLDExNywtNjIsLTExOSwtMTI4LDE4LDY1LDkwLDE0LC0xMDIsMjEsODUsMTAwLDExNyw1NSwyOSwxMjcsNTQsNzcsNzIsNzQsMzIsNzgsMywtMTExLDExOCwtNzAsLTg2LDEyNywtNzQsNjAsMjIsNDgsMzcsODcsMTMsMCwtMTA1LDUsLTEyMiwtNzEsLTEwMCwxMDQsLTEyNiwtMTYsNzksLTMwLDEyMCw3NywtNzYsLTQxLC0xMDksMiw5NywtMTAxLC0xLDE1LDEyMywxMTksMTA4LDkxLC0yMCwtMTI1LC05NiwyLC05MiwtMTUsLTY3LC03NiwxMjEsMTA0LDEwNSw2NCwtNjIsMTAyLDgsNCwxMjEsLTQ1LC04MCwtODEsLTgsMTE4LDQ0LC04MiwtNDEsLTg0LDczLC0zNiwxMTcsODAsLTY5LC03MywxNCwtMTgsNzIsMzEsLTUsLTExMSwtMTI3LC00MywzNCwtOCw1NywxMDMsLTQyLDE4LC0zMywxMTcsLTI2LC0xMjQsLTEyNCwxNSw4OCwyMywxNiwtNTcsNTQsLTYsLTEwMiwxMTYsLTk5LC00NSwxMDAsLTM1LDg3LDM3LDYsOTgsMiwxMTIsNjAsLTMzLDE3LDI2LDk5LC0xMDUsNDgsLTEwNCwtMTE5LDc4LDYsLTU4LDk1LDksNDEsLTE2LDk2LDQxLC0yMiw5Niw3MiwxMTYsLTk1LC0xMDUsLTM2LC0xMjMsLTU1LDkxLC00NiwtNywtOTIsMzksNDUsODQsMTYsLTEyNCwtMTIyLC02OCwxLC0yOCwxMjIsLTYwLDgyLDEwMywtNTQsLTkyLDI3LC05OSwtMTI4LDY1LDcsLTcyLC0xMjcsNjIsLTIyLDIsLTExLDE4LC04OSwtMTA2LC03NCw3MSw4NiwtMTE2LC0yNSwtMTE1LC05Niw1NywtMzQsMjIsLTEyNCwtMTI1LC00LC00MSw0MiwtNTcsLTEwMyw0NSw3OCwxNCwtMTA2LDExMSw5OCwtOTQsLTcxLDUsNzUsMTksLTEyNCwtMzAsMzQsLTUwLDc1LC04NCwtNTAsLTU2LDUxLC0xNSwtMzYsNjEsLTk0LC03OSwtNDUsMTI2LC03NywtMTA1LC0yLC05MywtNiw4LC0zLDYsLTQyLDQ2LDEyNSw1LC05OCwxMyw2NywtMTAsLTEzLC05NCwtNzgsLTEyNywxMjEsLTI2LC04LC0xMDEsLTkxLDEyMSwtNDAsLTEyNCwtNjQsODQsLTcyLDYzLDE5LC04NF0sIm1hdGVyaWFsU2V0U2VyaWFsTnVtYmVyIjoxLCJpdlBhcmFtZXRlclNwZWMiOnsiaXYiOlszLC0xMCwtODUsMTE0LC05MCwxMTUsNzcsNTUsNTQsMTUsMzgsODQsLTExNiwxNCwtNDAsMjhdfX0=", + "eventLeftScope":false, + "executionRoleArn":"arn:aws:iam::0123456789012:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig", + "configRuleArn":"arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-pdmyw1", + "configRuleName":"rule-ec2-test", + "configRuleId":"config-rule-pdmyw1", + "accountId":"0123456789012", + "evaluationMode":"DETECTIVE" + } diff --git a/packages/parser/tests/events/bedrockAgentEvent.json b/packages/parser/tests/events/bedrockAgentEvent.json new file mode 100644 index 0000000000..b7ad75b3c4 --- /dev/null +++ b/packages/parser/tests/events/bedrockAgentEvent.json @@ -0,0 +1,16 @@ +{ + "actionGroup": "ClaimManagementActionGroup", + "messageVersion": "1.0", + "sessionId": "12345678912345", + "sessionAttributes": {}, + "promptSessionAttributes": {}, + "inputText": "I want to claim my insurance", + "agent": { + "alias": "TSTALIASID", + "name": "test", + "version": "DRAFT", + "id": "8ZXY0W8P1H" + }, + "httpMethod": "GET", + "apiPath": "/claims" +} diff --git a/packages/parser/tests/events/bedrockAgentPostEvent.json b/packages/parser/tests/events/bedrockAgentPostEvent.json new file mode 100644 index 0000000000..f223bfcd51 --- /dev/null +++ b/packages/parser/tests/events/bedrockAgentPostEvent.json @@ -0,0 +1,35 @@ +{ + "actionGroup": "ClaimManagementActionGroup", + "messageVersion": "1.0", + "sessionId": "12345678912345", + "sessionAttributes": {}, + "promptSessionAttributes": {}, + "inputText": "Send reminders to all pending documents", + "agent": { + "alias": "TSTALIASID", + "name": "test", + "version": "DRAFT", + "id": "8ZXY0W8P1H" + }, + "httpMethod": "POST", + "apiPath": "/send-reminders", + "requestBody": { + "content": { + "application/json": { + "properties": [ + { + "name": "claimId", + "type": "string", + "value": "20" + }, + { + "name": "pendingDocuments", + "type": "string", + "value": "social number and vat" + } + ] + } + } + }, + "parameters": [] +} diff --git a/packages/parser/tests/events/cloudFormationCustomResourceCreateEvent.json b/packages/parser/tests/events/cloudFormationCustomResourceCreateEvent.json new file mode 100644 index 0000000000..5c32d8c7aa --- /dev/null +++ b/packages/parser/tests/events/cloudFormationCustomResourceCreateEvent.json @@ -0,0 +1,13 @@ +{ + "RequestType": "Create", + "ServiceToken": "arn:aws:lambda:us-east-1:xxx:function:xxxx-CrbuiltinfunctionidProvi-2vKAalSppmKe", + "ResponseURL": "https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/7F%7Cb1f50fdfc25f3b", + "StackId": "arn:aws:cloudformation:us-east-1:xxxx:stack/xxxx/271845b0-f2e8-11ed-90ac-0eeb25b8ae21", + "RequestId": "xxxxx-d2a0-4dfb-ab1f-xxxxxx", + "LogicalResourceId": "xxxxxxxxx", + "ResourceType": "Custom::MyType", + "ResourceProperties": { + "ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx", + "MyProps": "ss" + } +} \ No newline at end of file diff --git a/packages/parser/tests/events/cloudFormationCustomResourceDeleteEvent.json b/packages/parser/tests/events/cloudFormationCustomResourceDeleteEvent.json new file mode 100644 index 0000000000..f26738133d --- /dev/null +++ b/packages/parser/tests/events/cloudFormationCustomResourceDeleteEvent.json @@ -0,0 +1,13 @@ +{ + "RequestType": "Delete", + "ServiceToken": "arn:aws:lambda:us-east-1:xxx:function:xxxx-CrbuiltinfunctionidProvi-2vKAalSppmKe", + "ResponseURL": "https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/7F%7Cb1f50fdfc25f3b", + "StackId": "arn:aws:cloudformation:us-east-1:xxxx:stack/xxxx/271845b0-f2e8-11ed-90ac-0eeb25b8ae21", + "RequestId": "xxxxx-d2a0-4dfb-ab1f-xxxxxx", + "LogicalResourceId": "xxxxxxxxx", + "ResourceType": "Custom::MyType", + "ResourceProperties": { + "ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx", + "MyProps": "ss" + } +} \ No newline at end of file diff --git a/packages/parser/tests/events/cloudFormationCustomResourceUpdateEvent.json b/packages/parser/tests/events/cloudFormationCustomResourceUpdateEvent.json new file mode 100644 index 0000000000..5225746345 --- /dev/null +++ b/packages/parser/tests/events/cloudFormationCustomResourceUpdateEvent.json @@ -0,0 +1,17 @@ +{ + "RequestType": "Update", + "ServiceToken": "arn:aws:lambda:us-east-1:xxx:function:xxxx-CrbuiltinfunctionidProvi-2vKAalSppmKe", + "ResponseURL": "https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/7F%7Cb1f50fdfc25f3b", + "StackId": "arn:aws:cloudformation:us-east-1:xxxx:stack/xxxx/271845b0-f2e8-11ed-90ac-0eeb25b8ae21", + "RequestId": "xxxxx-d2a0-4dfb-ab1f-xxxxxx", + "LogicalResourceId": "xxxxxxxxx", + "ResourceType": "Custom::MyType", + "ResourceProperties": { + "ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx", + "MyProps": "new" + }, + "OldResourceProperties": { + "ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx-xxxx-xxx", + "MyProps": "old" + } +} \ No newline at end of file diff --git a/packages/parser/tests/events/cloudWatchDashboardEvent.json b/packages/parser/tests/events/cloudWatchDashboardEvent.json new file mode 100644 index 0000000000..fd2d3be62d --- /dev/null +++ b/packages/parser/tests/events/cloudWatchDashboardEvent.json @@ -0,0 +1,38 @@ +{ + "original": "param-to-widget", + "widgetContext": { + "dashboardName": "Name-of-current-dashboard", + "widgetId": "widget-16", + "domain": "https://us-east-1.console.aws.amazon.com", + "accountId": "123456789123", + "locale": "en", + "timezone": { + "label": "UTC", + "offsetISO": "+00:00", + "offsetInMinutes": 0 + }, + "period": 300, + "isAutoPeriod": true, + "timeRange": { + "mode": "relative", + "start": 1627236199729, + "end": 1627322599729, + "relativeStart": 86400012, + "zoom": { + "start": 1627276030434, + "end": 1627282956521 + } + }, + "theme": "light", + "linkCharts": true, + "title": "Tweets for Amazon website problem", + "forms": { + "all": {} + }, + "params": { + "original": "param-to-widget" + }, + "width": 588, + "height": 369 + } +} diff --git a/packages/parser/tests/events/cloudWatchLogEvent.json b/packages/parser/tests/events/cloudWatchLogEvent.json new file mode 100644 index 0000000000..aa184c1d01 --- /dev/null +++ b/packages/parser/tests/events/cloudWatchLogEvent.json @@ -0,0 +1,5 @@ +{ + "awslogs": { + "data": "H4sIAAAAAAAAAHWPwQqCQBCGX0Xm7EFtK+smZBEUgXoLCdMhFtKV3akI8d0bLYmibvPPN3wz00CJxmQnTO41whwWQRIctmEcB6sQbFC3CjW3XW8kxpOpP+OC22d1Wml1qZkQGtoMsScxaczKN3plG8zlaHIta5KqWsozoTYw3/djzwhpLwivWFGHGpAFe7DL68JlBUk+l7KSN7tCOEJ4M3/qOI49vMHj+zCKdlFqLaU2ZHV2a4Ct/an0/ivdX8oYc1UVX860fQDQiMdxRQEAAA==" + } +} diff --git a/packages/parser/tests/events/codePipelineEvent.json b/packages/parser/tests/events/codePipelineEvent.json new file mode 100644 index 0000000000..d7abe51346 --- /dev/null +++ b/packages/parser/tests/events/codePipelineEvent.json @@ -0,0 +1,34 @@ +{ + "CodePipeline.job": { + "id": "11111111-abcd-1111-abcd-111111abcdef", + "accountId": "111111111111", + "data": { + "actionConfiguration": { + "configuration": { + "FunctionName": "MyLambdaFunctionForAWSCodePipeline", + "UserParameters": "some-input-such-as-a-URL" + } + }, + "inputArtifacts": [ + { + "name": "ArtifactName", + "revision": null, + "location": { + "type": "S3", + "s3Location": { + "bucketName": "the name of the bucket configured as the pipeline artifact store in Amazon S3, for example codepipeline-us-east-2-1234567890", + "objectKey": "the name of the application, for example CodePipelineDemoApplication.zip" + } + } + } + ], + "outputArtifacts": [], + "artifactCredentials": { + "accessKeyId": "AKIAIOSFODNN7EXAMPLE", + "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "sessionToken": "MIICiTCCAfICCQD6m7oRw0uXOjANBgkqhkiG9w0BAQUFADCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5jb20wHhcNMTEwNDI1MjA0NTIxWhcNMTIwNDI0MjA0NTIxWjCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMaK0dn+a4GmWIWJ21uUSfwfEvySWtC2XADZ4nB+BLYgVIk60CpiwsZ3G93vUEIO3IyNoH/f0wYK8m9TrDHudUZg3qX4waLG5M43q7Wgc/MbQITxOUSQv7c7ugFFDzQGBzZswY6786m86gpEIbb3OhjZnzcvQAaRHhdlQWIMm2nrAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAtCu4nUhVVxYUntneD9+h8Mg9q6q+auNKyExzyLwaxlAoo7TJHidbtS4J5iNmZgXL0FkbFFBjvSfpJIlJ00zbhNYS5f6GuoEDmFJl0ZxBHjJnyp378OD8uTs7fLvjx79LjSTbNYiytVbZPQUQ5Yaxu2jXnimvw3rrszlaEXAMPLE=" + }, + "continuationToken": "A continuation token if continuing job" + } + } +} diff --git a/packages/parser/tests/events/codePipelineEventData.json b/packages/parser/tests/events/codePipelineEventData.json new file mode 100644 index 0000000000..7552f19ca9 --- /dev/null +++ b/packages/parser/tests/events/codePipelineEventData.json @@ -0,0 +1,46 @@ +{ + "CodePipeline.job": { + "id": "c0d76431-b0e7-xmpl-97e3-e8ee786eb6f6", + "accountId": "123456789012", + "data": { + "actionConfiguration": { + "configuration": { + "FunctionName": "my-function", + "UserParameters": "{\"KEY\": \"VALUE\"}" + } + }, + "inputArtifacts": [ + { + "name": "my-pipeline-SourceArtifact", + "revision": "e0c7xmpl2308ca3071aa7bab414de234ab52eea", + "location": { + "type": "S3", + "s3Location": { + "bucketName": "us-west-2-123456789012-my-pipeline", + "objectKey": "my-pipeline/test-api-2/TdOSFRV" + } + } + } + ], + "outputArtifacts": [ + { + "name": "invokeOutput", + "revision": null, + "location": { + "type": "S3", + "s3Location": { + "bucketName": "us-west-2-123456789012-my-pipeline", + "objectKey": "my-pipeline/invokeOutp/D0YHsJn" + } + } + } + ], + "artifactCredentials": { + "accessKeyId": "AKIAIOSFODNN7EXAMPLE", + "secretAccessKey": "6CGtmAa3lzWtV7a...", + "sessionToken": "IQoJb3JpZ2luX2VjEA...", + "expirationTime": 1575493418000 + } + } + } +} diff --git a/packages/parser/tests/events/codePipelineEventEmptyUserParameters.json b/packages/parser/tests/events/codePipelineEventEmptyUserParameters.json new file mode 100644 index 0000000000..1a0dec6a15 --- /dev/null +++ b/packages/parser/tests/events/codePipelineEventEmptyUserParameters.json @@ -0,0 +1,32 @@ +{ + "CodePipeline.job": { + "id": "11111111-abcd-1111-abcd-111111abcdef", + "accountId": "111111111111", + "data": { + "actionConfiguration": { + "configuration": { + "FunctionName": "MyLambdaFunctionForAWSCodePipeline" + } + }, + "inputArtifacts": [ + { + "name": "ArtifactName", + "revision": null, + "location": { + "type": "S3", + "s3Location": { + "bucketName": "the name of the bucket configured as the pipeline artifact store in Amazon S3, for example codepipeline-us-east-2-1234567890", + "objectKey": "the name of the application, for example CodePipelineDemoApplication.zip" + } + } + } + ], + "outputArtifacts": [], + "artifactCredentials": { + "accessKeyId": "AKIAIOSFODNN7EXAMPLE", + "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "sessionToken": "MIICiTCCAfICCQD6m7oRw0uXOjANBgkqhkiG9w0BAQUFADCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5jb20wHhcNMTEwNDI1MjA0NTIxWhcNMTIwNDI0MjA0NTIxWjCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMaK0dn+a4GmWIWJ21uUSfwfEvySWtC2XADZ4nB+BLYgVIk60CpiwsZ3G93vUEIO3IyNoH/f0wYK8m9TrDHudUZg3qX4waLG5M43q7Wgc/MbQITxOUSQv7c7ugFFDzQGBzZswY6786m86gpEIbb3OhjZnzcvQAaRHhdlQWIMm2nrAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAtCu4nUhVVxYUntneD9+h8Mg9q6q+auNKyExzyLwaxlAoo7TJHidbtS4J5iNmZgXL0FkbFFBjvSfpJIlJ00zbhNYS5f6GuoEDmFJl0ZxBHjJnyp378OD8uTs7fLvjx79LjSTbNYiytVbZPQUQ5Yaxu2jXnimvw3rrszlaEXAMPLE=" + } + } + } +} diff --git a/packages/parser/tests/events/codePipelineEventWithEncryptionKey.json b/packages/parser/tests/events/codePipelineEventWithEncryptionKey.json new file mode 100644 index 0000000000..e4a8528e14 --- /dev/null +++ b/packages/parser/tests/events/codePipelineEventWithEncryptionKey.json @@ -0,0 +1,38 @@ +{ + "CodePipeline.job": { + "id": "11111111-abcd-1111-abcd-111111abcdef", + "accountId": "111111111111", + "data": { + "actionConfiguration": { + "configuration": { + "FunctionName": "MyLambdaFunctionForAWSCodePipeline", + "UserParameters": "some-input-such-as-a-URL" + } + }, + "inputArtifacts": [ + { + "name": "ArtifactName", + "revision": null, + "location": { + "type": "S3", + "s3Location": { + "bucketName": "the name of the bucket configured as the pipeline artifact store in Amazon S3, for example codepipeline-us-east-2-1234567890", + "objectKey": "the name of the application, for example CodePipelineDemoApplication.zip" + } + } + } + ], + "outputArtifacts": [], + "artifactCredentials": { + "accessKeyId": "AKIAIOSFODNN7EXAMPLE", + "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "sessionToken": "MIICiTCCAfICCQD6m7oRw0uXOjANBgkqhkiG9w0BAQUFADCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5jb20wHhcNMTEwNDI1MjA0NTIxWhcNMTIwNDI0MjA0NTIxWjCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMaK0dn+a4GmWIWJ21uUSfwfEvySWtC2XADZ4nB+BLYgVIk60CpiwsZ3G93vUEIO3IyNoH/f0wYK8m9TrDHudUZg3qX4waLG5M43q7Wgc/MbQITxOUSQv7c7ugFFDzQGBzZswY6786m86gpEIbb3OhjZnzcvQAaRHhdlQWIMm2nrAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAtCu4nUhVVxYUntneD9+h8Mg9q6q+auNKyExzyLwaxlAoo7TJHidbtS4J5iNmZgXL0FkbFFBjvSfpJIlJ00zbhNYS5f6GuoEDmFJl0ZxBHjJnyp378OD8uTs7fLvjx79LjSTbNYiytVbZPQUQ5Yaxu2jXnimvw3rrszlaEXAMPLE=" + }, + "continuationToken": "A continuation token if continuing job", + "encryptionKey": { + "id": "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", + "type": "KMS" + } + } + } +} diff --git a/packages/parser/tests/events/cognitoCreateAuthChallengeEvent.json b/packages/parser/tests/events/cognitoCreateAuthChallengeEvent.json new file mode 100644 index 0000000000..ad018ae082 --- /dev/null +++ b/packages/parser/tests/events/cognitoCreateAuthChallengeEvent.json @@ -0,0 +1,29 @@ +{ + "version": "1", + "region": "us-east-1", + "userPoolId": "us-east-1_example", + "userName": "UserName", + "callerContext": { + "awsSdkVersion": "awsSdkVersion", + "clientId": "clientId" + }, + "triggerSource": "CreateAuthChallenge_Authentication", + "request": { + "userAttributes": { + "sub": "4A709A36-7D63-4785-829D-4198EF10EBDA", + "email_verified": "true", + "name": "First Last", + "email": "create-auth@mail.com" + }, + "challengeName": "PASSWORD_VERIFIER", + "session" : [ + { + "challengeName": "CUSTOM_CHALLENGE", + "challengeResult": true, + "challengeMetadata": "CAPTCHA_CHALLENGE" + } + ], + "userNotFound": false + }, + "response": {} +} diff --git a/packages/parser/tests/events/cognitoCustomMessageEvent.json b/packages/parser/tests/events/cognitoCustomMessageEvent.json new file mode 100644 index 0000000000..8652c3bff4 --- /dev/null +++ b/packages/parser/tests/events/cognitoCustomMessageEvent.json @@ -0,0 +1,20 @@ +{ + "version": "1", + "triggerSource": "CustomMessage_AdminCreateUser", + "region": "region", + "userPoolId": "userPoolId", + "userName": "userName", + "callerContext": { + "awsSdk": "awsSdkVersion", + "clientId": "clientId" + }, + "request": { + "userAttributes": { + "phone_number_verified": false, + "email_verified": true + }, + "codeParameter": "####", + "usernameParameter": "username" + }, + "response": {} +} diff --git a/packages/parser/tests/events/cognitoDefineAuthChallengeEvent.json b/packages/parser/tests/events/cognitoDefineAuthChallengeEvent.json new file mode 100644 index 0000000000..80ea5ac2d9 --- /dev/null +++ b/packages/parser/tests/events/cognitoDefineAuthChallengeEvent.json @@ -0,0 +1,32 @@ +{ + "version": "1", + "region": "us-east-1", + "userPoolId": "us-east-1_example", + "userName": "UserName", + "callerContext": { + "awsSdkVersion": "awsSdkVersion", + "clientId": "clientId" + }, + "triggerSource": "DefineAuthChallenge_Authentication", + "request": { + "userAttributes": { + "sub": "4A709A36-7D63-4785-829D-4198EF10EBDA", + "email_verified": "true", + "name": "First Last", + "email": "define-auth@mail.com" + }, + "session" : [ + { + "challengeName": "PASSWORD_VERIFIER", + "challengeResult": true + }, + { + "challengeName": "CUSTOM_CHALLENGE", + "challengeResult": true, + "challengeMetadata": "CAPTCHA_CHALLENGE" + } + ], + "userNotFound": true + }, + "response": {} +} diff --git a/packages/parser/tests/events/cognitoPostAuthenticationEvent.json b/packages/parser/tests/events/cognitoPostAuthenticationEvent.json new file mode 100644 index 0000000000..d34b18eeba --- /dev/null +++ b/packages/parser/tests/events/cognitoPostAuthenticationEvent.json @@ -0,0 +1,18 @@ +{ + "version": "1", + "region": "us-east-1", + "userPoolId": "us-east-1_example", + "userName": "UserName", + "callerContext": { + "awsSdkVersion": "awsSdkVersion", + "clientId": "clientId" + }, + "triggerSource": "PostAuthentication_Authentication", + "request": { + "newDeviceUsed": true, + "userAttributes": { + "email": "post-auth@mail.com" + } + }, + "response": {} +} diff --git a/packages/parser/tests/events/cognitoPostConfirmationEvent.json b/packages/parser/tests/events/cognitoPostConfirmationEvent.json new file mode 100644 index 0000000000..e88f98150c --- /dev/null +++ b/packages/parser/tests/events/cognitoPostConfirmationEvent.json @@ -0,0 +1,18 @@ +{ + "version": "string", + "triggerSource": "PostConfirmation_ConfirmSignUp", + "region": "us-east-1", + "userPoolId": "string", + "userName": "userName", + "callerContext": { + "awsSdkVersion": "awsSdkVersion", + "clientId": "clientId" + }, + "request": { + "userAttributes": { + "email": "user@example.com", + "email_verified": true + } + }, + "response": {} +} diff --git a/packages/parser/tests/events/cognitoPreAuthenticationEvent.json b/packages/parser/tests/events/cognitoPreAuthenticationEvent.json new file mode 100644 index 0000000000..661fea6372 --- /dev/null +++ b/packages/parser/tests/events/cognitoPreAuthenticationEvent.json @@ -0,0 +1,20 @@ +{ + "version": "1", + "region": "us-east-1", + "userPoolId": "us-east-1_example", + "userName": "UserName", + "callerContext": { + "awsSdkVersion": "awsSdkVersion", + "clientId": "clientId" + }, + "triggerSource": "PreAuthentication_Authentication", + "request": { + "userAttributes": { + "sub": "4A709A36-7D63-4785-829D-4198EF10EBDA", + "email_verified": "true", + "name": "First Last", + "email": "pre-auth@mail.com" + } + }, + "response": {} +} diff --git a/packages/parser/tests/events/cognitoPreSignUpEvent.json b/packages/parser/tests/events/cognitoPreSignUpEvent.json new file mode 100644 index 0000000000..feb4eba25d --- /dev/null +++ b/packages/parser/tests/events/cognitoPreSignUpEvent.json @@ -0,0 +1,18 @@ +{ + "version": "string", + "triggerSource": "PreSignUp_SignUp", + "region": "us-east-1", + "userPoolId": "string", + "userName": "userName", + "callerContext": { + "awsSdkVersion": "awsSdkVersion", + "clientId": "clientId" + }, + "request": { + "userAttributes": { + "email": "user@example.com", + "phone_number": "+12065550100" + } + }, + "response": {} +} diff --git a/packages/parser/tests/events/cognitoPreTokenGenerationEvent.json b/packages/parser/tests/events/cognitoPreTokenGenerationEvent.json new file mode 100644 index 0000000000..f5ee69e0d2 --- /dev/null +++ b/packages/parser/tests/events/cognitoPreTokenGenerationEvent.json @@ -0,0 +1,25 @@ +{ + "version": "1", + "triggerSource": "TokenGeneration_Authentication", + "region": "us-west-2", + "userPoolId": "us-west-2_example", + "userName": "testqq", + "callerContext": { + "awsSdkVersion": "aws-sdk-unknown-unknown", + "clientId": "71ghuul37mresr7h373b704tua" + }, + "request": { + "userAttributes": { + "sub": "0b0a57c5-f013-426a-81a1-f8ffbfba21f0", + "email_verified": "true", + "cognito:user_status": "CONFIRMED", + "email": "test@mail.com" + }, + "groupConfiguration": { + "groupsToOverride": [], + "iamRolesToOverride": [], + "preferredRole": null + } + }, + "response": {} +} diff --git a/packages/parser/tests/events/cognitoUserMigrationEvent.json b/packages/parser/tests/events/cognitoUserMigrationEvent.json new file mode 100644 index 0000000000..2eae4e6618 --- /dev/null +++ b/packages/parser/tests/events/cognitoUserMigrationEvent.json @@ -0,0 +1,15 @@ +{ + "version": "string", + "triggerSource": "UserMigration_Authentication", + "region": "us-east-1", + "userPoolId": "string", + "userName": "userName", + "callerContext": { + "awsSdkVersion": "awsSdkVersion", + "clientId": "clientId" + }, + "request": { + "password": "password" + }, + "response": {} +} diff --git a/packages/parser/tests/events/cognitoVerifyAuthChallengeResponseEvent.json b/packages/parser/tests/events/cognitoVerifyAuthChallengeResponseEvent.json new file mode 100644 index 0000000000..2ebcdb5c27 --- /dev/null +++ b/packages/parser/tests/events/cognitoVerifyAuthChallengeResponseEvent.json @@ -0,0 +1,28 @@ +{ + "version": "1", + "region": "us-east-1", + "userPoolId": "us-east-1_example", + "userName": "UserName", + "callerContext": { + "awsSdkVersion": "awsSdkVersion", + "clientId": "clientId" + }, + "triggerSource": "VerifyAuthChallengeResponse_Authentication", + "request": { + "userAttributes": { + "sub": "4A709A36-7D63-4785-829D-4198EF10EBDA", + "email_verified": "true", + "name": "First Last", + "email": "verify-auth@mail.com" + }, + "privateChallengeParameters": { + "answer": "challengeAnswer" + }, + "clientMetadata" : { + "foo": "value" + }, + "challengeAnswer": "challengeAnswer", + "userNotFound": true + }, + "response": {} +} diff --git a/packages/parser/tests/events/connectContactFlowEventAll.json b/packages/parser/tests/events/connectContactFlowEventAll.json new file mode 100644 index 0000000000..5850649b6e --- /dev/null +++ b/packages/parser/tests/events/connectContactFlowEventAll.json @@ -0,0 +1,41 @@ +{ + "Name": "ContactFlowEvent", + "Details": { + "ContactData": { + "Attributes": { + "Language": "en-US" + }, + "Channel": "VOICE", + "ContactId": "5ca32fbd-8f92-46af-92a5-6b0f970f0efe", + "CustomerEndpoint": { + "Address": "+11234567890", + "Type": "TELEPHONE_NUMBER" + }, + "InitialContactId": "5ca32fbd-8f92-46af-92a5-6b0f970f0efe", + "InitiationMethod": "API", + "InstanceARN": "arn:aws:connect:eu-central-1:123456789012:instance/9308c2a1-9bc6-4cea-8290-6c0b4a6d38fa", + "MediaStreams": { + "Customer": { + "Audio": { + "StartFragmentNumber": "91343852333181432392682062622220590765191907586", + "StartTimestamp": "1565781909613", + "StreamARN": "arn:aws:kinesisvideo:eu-central-1:123456789012:stream/connect-contact-a3d73b84-ce0e-479a-a9dc-5637c9d30ac9/1565272947806" + } + } + }, + "PreviousContactId": "5ca32fbd-8f92-46af-92a5-6b0f970f0efe", + "Queue": { + "ARN": "arn:aws:connect:eu-central-1:123456789012:instance/9308c2a1-9bc6-4cea-8290-6c0b4a6d38fa/queue/5cba7cbf-1ecb-4b6d-b8bd-fe91079b3fc8", + "Name": "QueueOne" + }, + "SystemEndpoint": { + "Address": "+11234567890", + "Type": "TELEPHONE_NUMBER" + } + }, + "Parameters": { + "ParameterOne": "One", + "ParameterTwo": "Two" + } + } +} \ No newline at end of file diff --git a/packages/parser/tests/events/connectContactFlowEventMin.json b/packages/parser/tests/events/connectContactFlowEventMin.json new file mode 100644 index 0000000000..9cc22d59c3 --- /dev/null +++ b/packages/parser/tests/events/connectContactFlowEventMin.json @@ -0,0 +1,27 @@ +{ + "Name": "ContactFlowEvent", + "Details": { + "ContactData": { + "Attributes": {}, + "Channel": "VOICE", + "ContactId": "5ca32fbd-8f92-46af-92a5-6b0f970f0efe", + "CustomerEndpoint": null, + "InitialContactId": "5ca32fbd-8f92-46af-92a5-6b0f970f0efe", + "InitiationMethod": "API", + "InstanceARN": "arn:aws:connect:eu-central-1:123456789012:instance/9308c2a1-9bc6-4cea-8290-6c0b4a6d38fa", + "MediaStreams": { + "Customer": { + "Audio": { + "StartFragmentNumber": null, + "StartTimestamp": null, + "StreamARN": null + } + } + }, + "PreviousContactId": "5ca32fbd-8f92-46af-92a5-6b0f970f0efe", + "Queue": null, + "SystemEndpoint": null + }, + "Parameters": {} + } +} \ No newline at end of file diff --git a/packages/parser/tests/events/dynamoStreamEvent.json b/packages/parser/tests/events/dynamoStreamEvent.json new file mode 100644 index 0000000000..16009a7a95 --- /dev/null +++ b/packages/parser/tests/events/dynamoStreamEvent.json @@ -0,0 +1,65 @@ +{ + "Records": [ + { + "eventID": "1", + "eventVersion": "1.0", + "dynamodb": { + "ApproximateCreationDateTime": 1693997155.0, + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "StreamViewType": "NEW_AND_OLD_IMAGES", + "SequenceNumber": "111", + "SizeBytes": 26 + }, + "awsRegion": "us-west-2", + "eventName": "INSERT", + "eventSourceARN": "eventsource_arn", + "eventSource": "aws:dynamodb" + }, + { + "eventID": "2", + "eventVersion": "1.0", + "dynamodb": { + "OldImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "SequenceNumber": "222", + "Keys": { + "Id": { + "N": "101" + } + }, + "SizeBytes": 59, + "NewImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "awsRegion": "us-west-2", + "eventName": "MODIFY", + "eventSourceARN": "source_arn", + "eventSource": "aws:dynamodb" + } + ] +} diff --git a/packages/parser/tests/events/eventBridgeEvent.json b/packages/parser/tests/events/eventBridgeEvent.json new file mode 100644 index 0000000000..65872cf9a3 --- /dev/null +++ b/packages/parser/tests/events/eventBridgeEvent.json @@ -0,0 +1,17 @@ +{ + "version": "0", + "id": "6a7e8feb-b491-4cf7-a9f1-bf3703467718", + "detail-type": "EC2 Instance State-change Notification", + "source": "aws.ec2", + "account": "111122223333", + "time": "2017-12-22T18:43:48Z", + "region": "us-west-1", + "resources": [ + "arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0" + ], + "detail": { + "instance_id": "i-1234567890abcdef0", + "state": "terminated" + }, + "replay-name": "replay_archive" +} diff --git a/packages/parser/tests/events/kafkaEventMsk.json b/packages/parser/tests/events/kafkaEventMsk.json new file mode 100644 index 0000000000..5a35b89680 --- /dev/null +++ b/packages/parser/tests/events/kafkaEventMsk.json @@ -0,0 +1,35 @@ +{ + "eventSource":"aws:kafka", + "eventSourceArn":"arn:aws:kafka:us-east-1:0123456789019:cluster/SalesCluster/abcd1234-abcd-cafe-abab-9876543210ab-4", + "bootstrapServers":"b-2.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092,b-1.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092", + "records":{ + "mytopic-0":[ + { + "topic":"mytopic", + "partition":0, + "offset":15, + "timestamp":1545084650987, + "timestampType":"CREATE_TIME", + "key":"cmVjb3JkS2V5", + "value":"eyJrZXkiOiJ2YWx1ZSJ9", + "headers":[ + { + "headerKey":[ + 104, + 101, + 97, + 100, + 101, + 114, + 86, + 97, + 108, + 117, + 101 + ] + } + ] + } + ] + } +} diff --git a/packages/parser/tests/events/kafkaEventSelfManaged.json b/packages/parser/tests/events/kafkaEventSelfManaged.json new file mode 100644 index 0000000000..22985dd11d --- /dev/null +++ b/packages/parser/tests/events/kafkaEventSelfManaged.json @@ -0,0 +1,34 @@ +{ + "eventSource":"aws:SelfManagedKafka", + "bootstrapServers":"b-2.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092,b-1.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092", + "records":{ + "mytopic-0":[ + { + "topic":"mytopic", + "partition":0, + "offset":15, + "timestamp":1545084650987, + "timestampType":"CREATE_TIME", + "key":"cmVjb3JkS2V5", + "value":"eyJrZXkiOiJ2YWx1ZSJ9", + "headers":[ + { + "headerKey":[ + 104, + 101, + 97, + 100, + 101, + 114, + 86, + 97, + 108, + 117, + 101 + ] + } + ] + } + ] + } +} diff --git a/packages/parser/tests/events/kinesisFirehoseKinesisEvent.json b/packages/parser/tests/events/kinesisFirehoseKinesisEvent.json new file mode 100644 index 0000000000..6cdd8e8a5b --- /dev/null +++ b/packages/parser/tests/events/kinesisFirehoseKinesisEvent.json @@ -0,0 +1,32 @@ +{ + "invocationId": "2b4d1ad9-2f48-94bd-a088-767c317e994a", + "sourceKinesisStreamArn":"arn:aws:kinesis:us-east-1:123456789012:stream/kinesis-source", + "deliveryStreamArn": "arn:aws:firehose:us-east-2:123456789012:deliverystream/delivery-stream-name", + "region": "us-east-2", + "records": [ + { + "data": "SGVsbG8gV29ybGQ=", + "recordId": "record1", + "approximateArrivalTimestamp": 1664028820148, + "kinesisRecordMetadata": { + "shardId": "shardId-000000000000", + "partitionKey": "4d1ad2b9-24f8-4b9d-a088-76e9947c317a", + "approximateArrivalTimestamp": 1664028820148, + "sequenceNumber": "49546986683135544286507457936321625675700192471156785154", + "subsequenceNumber": 0 + } + }, + { + "data": "eyJIZWxsbyI6ICJXb3JsZCJ9", + "recordId": "record2", + "approximateArrivalTimestamp": 1664028793294, + "kinesisRecordMetadata": { + "shardId": "shardId-000000000001", + "partitionKey": "4d1ad2b9-24f8-4b9d-a088-76e9947c318a", + "approximateArrivalTimestamp": 1664028793294, + "sequenceNumber": "49546986683135544286507457936321625675700192471156785155", + "subsequenceNumber": 0 + } + } + ] +} diff --git a/packages/parser/tests/events/kinesisFirehosePutEvent.json b/packages/parser/tests/events/kinesisFirehosePutEvent.json new file mode 100644 index 0000000000..f3e0719071 --- /dev/null +++ b/packages/parser/tests/events/kinesisFirehosePutEvent.json @@ -0,0 +1,17 @@ +{ + "invocationId": "2b4d1ad9-2f48-94bd-a088-767c317e994a", + "deliveryStreamArn": "arn:aws:firehose:us-east-2:123456789012:deliverystream/delivery-stream-name", + "region": "us-east-2", + "records": [ + { + "recordId": "record1", + "approximateArrivalTimestamp": 1664029185290, + "data": "SGVsbG8gV29ybGQ=" + }, + { + "recordId": "record2", + "approximateArrivalTimestamp": 1664029186945, + "data": "eyJIZWxsbyI6ICJXb3JsZCJ9" + } + ] + } diff --git a/packages/parser/tests/events/kinesisFirehoseSQSEvent.json b/packages/parser/tests/events/kinesisFirehoseSQSEvent.json new file mode 100644 index 0000000000..bea267c420 --- /dev/null +++ b/packages/parser/tests/events/kinesisFirehoseSQSEvent.json @@ -0,0 +1,12 @@ +{ + "invocationId": "556b67a3-48fc-4385-af49-e133aade9cb9", + "deliveryStreamArn": "arn:aws:firehose:us-east-1:123456789012:deliverystream/PUT-S3-tdyyE", + "region": "us-east-1", + "records": [ + { + "recordId": "49640912821178817833517986466168945147170627572855734274000000", + "approximateArrivalTimestamp": 1684864917398, + "data": "eyJtZXNzYWdlSWQiOiI1YWI4MDdkNC01NjQ0LTRjNTUtOTdhMy00NzM5NjYzNWFjNzQiLCJyZWNlaXB0SGFuZGxlIjoiQVFFQndKbkt5ckhpZ1VNWmo2cllpZ0NneGxhUzNTTHkwYS4uLiIsImJvZHkiOiJUZXN0IG1lc3NhZ2UuIiwiYXR0cmlidXRlcyI6eyJBcHByb3hpbWF0ZVJlY2VpdmVDb3VudCI6IjEiLCJTZW50VGltZXN0YW1wIjoiMTY4NDg2NDg1MjQ5MSIsIlNlbmRlcklkIjoiQUlEQUlFTlFaSk9MTzIzWVZKNFZPIiwiQXBwcm94aW1hdGVGaXJzdFJlY2VpdmVUaW1lc3RhbXAiOiIxNjg0ODY0ODcyNDkxIn0sIm1lc3NhZ2VBdHRyaWJ1dGVzIjp7fSwibWQ1T2ZNZXNzYWdlQXR0cmlidXRlcyI6bnVsbCwibWQ1T2ZCb2R5IjoiYzhiNmJjNjBjOGI4YjNhOTA0ZTQ1YzFmYWJkZjUyM2QiLCJldmVudFNvdXJjZSI6ImF3czpzcXMiLCJldmVudFNvdXJjZUFSTiI6ImFybjphd3M6c3FzOnVzLWVhc3QtMToyMDA5ODQxMTIzODY6U05TIiwiYXdzUmVnaW9uIjoidXMtZWFzdC0xIn0K" + } + ] +} diff --git a/packages/parser/tests/events/kinesisStreamCloudWatchLogsEvent.json b/packages/parser/tests/events/kinesisStreamCloudWatchLogsEvent.json new file mode 100644 index 0000000000..a9a6959f90 --- /dev/null +++ b/packages/parser/tests/events/kinesisStreamCloudWatchLogsEvent.json @@ -0,0 +1,36 @@ +{ + "Records": [ + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "da10bf66b1f54bff5d96eae99149ad1f", + "sequenceNumber": "49635052289529725553291405521504870233219489715332317186", + "data": "H4sIAAAAAAAAAK2Sa2vbMBSG/4ox+xg3Oror39IlvaztVmJv7WjCUGwl8+ZLZstts5L/vuOsZYUyWGEgJHiP9J7nvOghLF3b2rVLthsXjsLJOBl/uZjG8fh4Gg7C+q5yDcqUAWcSONHEoFzU6+Om7jZYGdq7dljYcpnZ4cZHwLWOJl1Zbs/r9cR6e9RVqc/rKlpXV9eXt+fy27vt8W+L2DfOlr07oXQIMAQyvHlzPk6mcbKgciktF5lQfMU5dZZqzrShLF2uFC60aLtlmzb5prc/ygvvmjYc3YRPFG+LusuurE+/Ikqb1Gd55dq8jV+8isT6+317Rk42J5PTcLFnm966yvd2D2GeISJTYIwCJSQ1BE9OtWZCABWaKMIJAMdDMyU5MYZLhmkxBhQxfY4Re1tiWiAlBsgIVQTE4Cl6tI+T8SwJZu5Hh1dPs1FApOMSDI9WVKmIC+4irTMWQZYpx7QkztrgE06MU4yCx9DmVbgbvABmQJTGtkYAB0NwEwyYQUBpqEFuSbkGrThTRKi/AlP+HHj6fvJa3P9Ap/+Rbja9/PD6POd+0jXW7xM1B8CDsp37w7woXBb8qQDZ6xeurJttEOc/HWpUBxeHKNr74LHwsXXYlsm9flrl/rmFIQeS7m3m1fVs/DlIGpu6nhMiyWQGXNKIMbcCIgkhElKbaZnZpYJUz33s1iV+z/6+StMlR3yphHNcCyxiNEXf2zed6xuEu8XuF2wb6krnAwAA", + "approximateArrivalTimestamp": 1668093033.744 + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000000:49635052289529725553291405521504870233219489715332317186", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn:aws:iam::231436140809:role/pt-1488-CloudWatchKinesisLogsFunctionRole-1M4G2TIWIE49", + "awsRegion": "eu-west-1", + "eventSourceARN": "arn:aws:kinesis:eu-west-1:231436140809:stream/pt-1488-KinesisStreamCloudWatchLogs-D8tHs0im0aJG" + }, + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "cf4c4c2c9a49bdfaf58d7dbbc2b06081", + "sequenceNumber": "49635052289529725553291405520881064510298312199003701250", + "data": "H4sIAAAAAAAAAK2SW2/TQBCF/4pl8ViTvc7u5i0laVraQhUbWtREaG1PgsGXYK/bhqr/nXVoBRIgUYnXc2bPfHO092GFXWc3mOy2GI7D6SSZfDyfxfFkPgsPwua2xtbLjFPBgQqiifFy2WzmbdNvvTOyt92otFWa29HWRVRoHU37qtqdNZupdfaorzNXNHW0qS+vLm7O4PPr3fxHROxatNWQThgbUTqiZHT94mySzOJkBUqYLOWY8ZQLbaTRkEvDciUYzWzKfETXp13WFtsh/qgoHbZdOL4OnyhelU2fX1qXffIoXdKcFjV2RRf/9iqSmy933Sk53h5PT8LVnm12g7Ub4u7DIveIXFFjFNGUKUlAaMY0EUJKLjkQbxhKGCWeknMKoAGUkYoJ7TFd4St2tvJtDRYxDAg3VB08Ve/j42SySIIFfu396Ek+DkS+xkwAiYhM00isgUV6jXmEMrM5EmMsh+C9v9hfMQ4eS1vW4cPBH4CZVpoTJkEIAp5RUMo8vGFae3JNCCdUccMVgPw7sP4VePZm+lzc/0AH/0i3mF28fX6fSzftW+v2jZKXRgVVt3SHRVliHvx06F4+x6ppd0FcfEMvMR2cH3rR3gWPxrsO/Vau9vqyvlpMPgRJazMcYGgEHHLKBhLGJaBA0JLxNc0JppoS9Cwxbir/B4d5QDBAQSnfFFGp8aa/vxw2uLbHYUH4sHr4Dj5RJxfMAwAA", + "approximateArrivalTimestamp": 1668092612.992 + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000000:49635052289529725553291405520881064510298312199003701250", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn:aws:iam::231436140809:role/pt-1488-CloudWatchKinesisLogsFunctionRole-1M4G2TIWIE49", + "awsRegion": "eu-west-1", + "eventSourceARN": "arn:aws:kinesis:eu-west-1:231436140809:stream/pt-1488-KinesisStreamCloudWatchLogs-D8tHs0im0aJG" + } + ] +} \ No newline at end of file diff --git a/packages/parser/tests/events/kinesisStreamEvent.json b/packages/parser/tests/events/kinesisStreamEvent.json new file mode 100644 index 0000000000..ef8e209638 --- /dev/null +++ b/packages/parser/tests/events/kinesisStreamEvent.json @@ -0,0 +1,36 @@ +{ + "Records": [ + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "1", + "sequenceNumber": "49590338271490256608559692538361571095921575989136588898", + "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0Lg==", + "approximateArrivalTimestamp": 1545084650.987 + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000006:49590338271490256608559692538361571095921575989136588898", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn:aws:iam::123456789012:role/lambda-role", + "awsRegion": "us-east-2", + "eventSourceARN": "arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream" + }, + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "1", + "sequenceNumber": "49590338271490256608559692540925702759324208523137515618", + "data": "VGhpcyBpcyBvbmx5IGEgdGVzdC4=", + "approximateArrivalTimestamp": 1545084711.166 + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000006:49590338271490256608559692540925702759324208523137515618", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn:aws:iam::123456789012:role/lambda-role", + "awsRegion": "us-east-2", + "eventSourceARN": "arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream" + } + ] +} diff --git a/packages/parser/tests/events/kinesisStreamEventOneRecord.json b/packages/parser/tests/events/kinesisStreamEventOneRecord.json new file mode 100644 index 0000000000..05fe2d297a --- /dev/null +++ b/packages/parser/tests/events/kinesisStreamEventOneRecord.json @@ -0,0 +1,20 @@ +{ + "Records": [ + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "1", + "sequenceNumber": "49590338271490256608559692538361571095921575989136588898", + "data": "eyJtZXNzYWdlIjogInRlc3QgbWVzc2FnZSIsICJ1c2VybmFtZSI6ICJ0ZXN0In0=", + "approximateArrivalTimestamp": 1545084650.987 + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000006:49590338271490256608559692538361571095921575989136588898", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn:aws:iam::123456789012:role/lambda-role", + "awsRegion": "us-east-2", + "eventSourceARN": "arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream" + } + ] +} diff --git a/packages/parser/tests/events/lambdaFunctionUrlEvent.json b/packages/parser/tests/events/lambdaFunctionUrlEvent.json new file mode 100644 index 0000000000..da5c133e6f --- /dev/null +++ b/packages/parser/tests/events/lambdaFunctionUrlEvent.json @@ -0,0 +1,47 @@ +{ + "version":"2.0", + "routeKey":"$default", + "rawPath":"/", + "rawQueryString":"", + "headers":{ + "sec-fetch-mode":"navigate", + "x-amzn-tls-version":"TLSv1.2", + "sec-fetch-site":"cross-site", + "accept-language":"pt-BR,pt;q=0.9", + "x-forwarded-proto":"https", + "x-forwarded-port":"443", + "x-forwarded-for":"123.123.123.123", + "sec-fetch-user":"?1", + "accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + "x-amzn-tls-cipher-suite":"ECDHE-RSA-AES128-GCM-SHA256", + "sec-ch-ua":"\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"102\", \"Google Chrome\";v=\"102\"", + "sec-ch-ua-mobile":"?0", + "x-amzn-trace-id":"Root=1-62ecd163-5f302e550dcde3b12402207d", + "sec-ch-ua-platform":"\"Linux\"", + "host":".lambda-url.us-east-1.on.aws", + "upgrade-insecure-requests":"1", + "cache-control":"max-age=0", + "accept-encoding":"gzip, deflate, br", + "sec-fetch-dest":"document", + "user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36" + }, + "requestContext":{ + "accountId":"anonymous", + "apiId":"", + "domainName":".lambda-url.us-east-1.on.aws", + "domainPrefix":"", + "http":{ + "method":"GET", + "path":"/", + "protocol":"HTTP/1.1", + "sourceIp":"123.123.123.123", + "userAgent":"agent" + }, + "requestId":"id", + "routeKey":"$default", + "stage":"$default", + "time":"05/Aug/2022:08:14:39 +0000", + "timeEpoch":1659687279885 + }, + "isBase64Encoded":false +} diff --git a/packages/parser/tests/events/lambdaFunctionUrlEventPathTrailingSlash.json b/packages/parser/tests/events/lambdaFunctionUrlEventPathTrailingSlash.json new file mode 100644 index 0000000000..b1f8226518 --- /dev/null +++ b/packages/parser/tests/events/lambdaFunctionUrlEventPathTrailingSlash.json @@ -0,0 +1,52 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path/", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "", + "authentication": null, + "authorizer": { + "iam": { + "accessKey": "AKIA...", + "accountId": "111122223333", + "callerId": "AIDA...", + "cognitoIdentity": null, + "principalOrgId": null, + "userArn": "arn:aws:iam::111122223333:user/example-user", + "userId": "AIDA..." + } + }, + "domainName": ".lambda-url.us-west-2.on.aws", + "domainPrefix": "", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "123.123.123.123", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "Hello from client!", + "pathParameters": null, + "isBase64Encoded": false, + "stageVariables": null + } \ No newline at end of file diff --git a/packages/parser/tests/events/lambdaFunctionUrlIAMEvent.json b/packages/parser/tests/events/lambdaFunctionUrlIAMEvent.json new file mode 100644 index 0000000000..bf52342b66 --- /dev/null +++ b/packages/parser/tests/events/lambdaFunctionUrlIAMEvent.json @@ -0,0 +1,52 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "", + "authentication": null, + "authorizer": { + "iam": { + "accessKey": "AKIA...", + "accountId": "111122223333", + "callerId": "AIDA...", + "cognitoIdentity": null, + "principalOrgId": null, + "userArn": "arn:aws:iam::111122223333:user/example-user", + "userId": "AIDA..." + } + }, + "domainName": ".lambda-url.us-west-2.on.aws", + "domainPrefix": "", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "123.123.123.123", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "Hello from client!", + "pathParameters": null, + "isBase64Encoded": false, + "stageVariables": null +} diff --git a/packages/parser/tests/events/rabbitMQEvent.json b/packages/parser/tests/events/rabbitMQEvent.json new file mode 100644 index 0000000000..e4259555a8 --- /dev/null +++ b/packages/parser/tests/events/rabbitMQEvent.json @@ -0,0 +1,51 @@ +{ + "eventSource": "aws:rmq", + "eventSourceArn": "arn:aws:mq:us-west-2:112556298976:broker:pizzaBroker:b-9bcfa592-423a-4942-879d-eb284b418fc8", + "rmqMessagesByQueue": { + "pizzaQueue::/": [ + { + "basicProperties": { + "contentType": "text/plain", + "contentEncoding": null, + "headers": { + "header1": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 49 + ] + }, + "header2": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 50 + ] + }, + "numberInHeader": 10 + }, + "deliveryMode": 1, + "priority": 34, + "correlationId": null, + "replyTo": null, + "expiration": "60000", + "messageId": null, + "timestamp": "Jan 1, 1970, 12:33:41 AM", + "type": null, + "userId": "AIDACKCEVSQ6C2EXAMPLE", + "appId": null, + "clusterId": null, + "bodySize": 80 + }, + "redelivered": false, + "data": "eyJ0aW1lb3V0IjowLCJkYXRhIjoiQ1pybWYwR3c4T3Y0YnFMUXhENEUifQ==" + } + ] + } +} diff --git a/packages/parser/tests/events/s3Event.json b/packages/parser/tests/events/s3Event.json new file mode 100644 index 0000000000..4558dc3c9e --- /dev/null +++ b/packages/parser/tests/events/s3Event.json @@ -0,0 +1,38 @@ +{ + "Records": [ + { + "eventVersion": "2.1", + "eventSource": "aws:s3", + "awsRegion": "us-east-2", + "eventTime": "2019-09-03T19:37:27.192Z", + "eventName": "ObjectCreated:Put", + "userIdentity": { + "principalId": "AWS:AIDAINPONIXQXHT3IKHL2" + }, + "requestParameters": { + "sourceIPAddress": "205.255.255.255" + }, + "responseElements": { + "x-amz-request-id": "D82B88E5F771F645", + "x-amz-id-2": "vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo=" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "828aa6fc-f7b5-4305-8584-487c791949c1", + "bucket": { + "name": "lambda-artifacts-deafc19498e3f2df", + "ownerIdentity": { + "principalId": "A3I5XTEXAMAI3E" + }, + "arn": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df" + }, + "object": { + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "size": 1305107, + "eTag": "b21b84d653bb07b05b1e6b33684dc11b", + "sequencer": "0C0F6F405D6ED209E1" + } + } + } + ] +} diff --git a/packages/parser/tests/events/s3EventBridgeNotificationObjectCreatedEvent.json b/packages/parser/tests/events/s3EventBridgeNotificationObjectCreatedEvent.json new file mode 100644 index 0000000000..5cc8f2f402 --- /dev/null +++ b/packages/parser/tests/events/s3EventBridgeNotificationObjectCreatedEvent.json @@ -0,0 +1,28 @@ +{ + "version": "0", + "id": "f5f1e65c-dc3a-93ca-6c1e-b1647eac7963", + "detail-type": "Object Created", + "source": "aws.s3", + "account": "123456789012", + "time": "2023-03-08T17:50:14Z", + "region": "eu-west-1", + "resources": [ + "arn:aws:s3:::example-bucket" + ], + "detail": { + "version": "0", + "bucket": { + "name": "example-bucket" + }, + "object": { + "key": "IMG_m7fzo3.jpg", + "size": 184662, + "etag": "4e68adba0abe2dc8653dc3354e14c01d", + "sequencer": "006408CAD69598B05E" + }, + "request-id": "57H08PA84AB1JZW0", + "requester": "123456789012", + "source-ip-address": "34.252.34.74", + "reason": "PutObject" + } +} \ No newline at end of file diff --git a/packages/parser/tests/events/s3EventBridgeNotificationObjectDeletedEvent.json b/packages/parser/tests/events/s3EventBridgeNotificationObjectDeletedEvent.json new file mode 100644 index 0000000000..af52ee2fef --- /dev/null +++ b/packages/parser/tests/events/s3EventBridgeNotificationObjectDeletedEvent.json @@ -0,0 +1,29 @@ +{ + "version": "0", + "id": "2ee9cc15-d022-99ea-1fb8-1b1bac4850f9", + "detail-type": "Object Deleted", + "source": "aws.s3", + "account": "111122223333", + "time": "2021-11-12T00:00:00Z", + "region": "ca-central-1", + "resources": [ + "arn:aws:s3:::example-bucket" + ], + "detail": { + "version": "0", + "bucket": { + "name": "example-bucket" + }, + "object": { + "key": "IMG_m7fzo3.jpg", + "size": 184662, + "etag": "4e68adba0abe2dc8653dc3354e14c01d", + "sequencer": "006408CAD69598B05E" + }, + "request-id": "0BH729840619AG5K", + "requester": "123456789012", + "source-ip-address": "34.252.34.74", + "reason": "DeleteObject", + "deletion-type": "Delete Marker Created" + } +} \ No newline at end of file diff --git a/packages/parser/tests/events/s3EventBridgeNotificationObjectExpiredEvent.json b/packages/parser/tests/events/s3EventBridgeNotificationObjectExpiredEvent.json new file mode 100644 index 0000000000..ef506cc355 --- /dev/null +++ b/packages/parser/tests/events/s3EventBridgeNotificationObjectExpiredEvent.json @@ -0,0 +1,28 @@ +{ + "version": "0", + "id": "ad1de317-e409-eba2-9552-30113f8d88e3", + "detail-type": "Object Deleted", + "source": "aws.s3", + "account": "111122223333", + "time": "2021-11-12T00:00:00Z", + "region": "ca-central-1", + "resources": [ + "arn:aws:s3:::example-bucket" + ], + "detail": { + "version": "0", + "bucket": { + "name": "example-bucket" + }, + "object": { + "key": "IMG_m7fzo3.jpg", + "size": 184662, + "etag": "4e68adba0abe2dc8653dc3354e14c01d", + "sequencer": "006408CAD69598B05E" + }, + "request-id": "20EB74C14654DC47", + "requester": "s3.amazonaws.com", + "reason": "Lifecycle Expiration", + "deletion-type": "Delete Marker Created" + } +} \ No newline at end of file diff --git a/packages/parser/tests/events/s3EventBridgeNotificationObjectRestoreCompletedEvent.json b/packages/parser/tests/events/s3EventBridgeNotificationObjectRestoreCompletedEvent.json new file mode 100644 index 0000000000..5a2e6a4f9e --- /dev/null +++ b/packages/parser/tests/events/s3EventBridgeNotificationObjectRestoreCompletedEvent.json @@ -0,0 +1,28 @@ +{ + "version": "0", + "id": "6924de0d-13e2-6bbf-c0c1-b903b753565e", + "detail-type": "Object Restore Completed", + "source": "aws.s3", + "account": "111122223333", + "time": "2021-11-12T00:00:00Z", + "region": "ca-central-1", + "resources": [ + "arn:aws:s3:::example-bucket" + ], + "detail": { + "version": "0", + "bucket": { + "name": "example-bucket" + }, + "object": { + "key": "IMG_m7fzo3.jpg", + "size": 184662, + "etag": "4e68adba0abe2dc8653dc3354e14c01d", + "sequencer": "006408CAD69598B05E" + }, + "request-id": "189F19CB7FB1B6A4", + "requester": "s3.amazonaws.com", + "restore-expiry-time": "2021-11-13T00:00:00Z", + "source-storage-class": "GLACIER" + } +} \ No newline at end of file diff --git a/packages/parser/tests/events/s3EventDecodedKey.json b/packages/parser/tests/events/s3EventDecodedKey.json new file mode 100644 index 0000000000..05f5ab5c4b --- /dev/null +++ b/packages/parser/tests/events/s3EventDecodedKey.json @@ -0,0 +1,40 @@ +{ + "Records": [ + { + "eventVersion": "2.0", + "eventSource": "aws:s3", + "awsRegion": "us-east-1", + "eventTime": "1970-01-01T00:00:00.123Z", + "eventName": "ObjectCreated:Put", + "userIdentity": { + "principalId": "EXAMPLE" + }, + "requestParameters": { + "sourceIPAddress": "127.0.0.1" + }, + "responseElements": { + "x-amz-request-id": "C3D13FE58DE4C810", + "x-amz-id-2": "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "testConfigRule", + "bucket": { + "name": "sourcebucket", + "ownerIdentity": { + "principalId": "EXAMPLE" + }, + "arn": "arn:aws:s3:::mybucket" + }, + "object": { + "key": "Happy%20Face.jpg", + "urlDecodedKey": "Happy Face.jpg", + "size": 1024, + "versionId": "version", + "eTag": "d41d8cd98f00b204e9800998ecf8427e", + "sequencer": "Happy Sequencer" + } + } + } + ] +} diff --git a/packages/parser/tests/events/s3EventDeleteObject.json b/packages/parser/tests/events/s3EventDeleteObject.json new file mode 100644 index 0000000000..3a607242f0 --- /dev/null +++ b/packages/parser/tests/events/s3EventDeleteObject.json @@ -0,0 +1,36 @@ +{ + "Records": [ + { + "eventVersion": "2.1", + "eventSource": "aws:s3", + "awsRegion": "us-east-2", + "eventTime": "2019-09-03T19:37:27.192Z", + "eventName": "ObjectRemoved:Delete", + "userIdentity": { + "principalId": "AWS:AIDAINPONIXQXHT3IKHL2" + }, + "requestParameters": { + "sourceIPAddress": "205.255.255.255" + }, + "responseElements": { + "x-amz-request-id": "D82B88E5F771F645", + "x-amz-id-2": "vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo=" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "828aa6fc-f7b5-4305-8584-487c791949c1", + "bucket": { + "name": "lambda-artifacts-deafc19498e3f2df", + "ownerIdentity": { + "principalId": "A3I5XTEXAMAI3E" + }, + "arn": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df" + }, + "object": { + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "sequencer": "0C0F6F405D6ED209E1" + } + } + } + ] +} \ No newline at end of file diff --git a/packages/parser/tests/events/s3EventGlacier.json b/packages/parser/tests/events/s3EventGlacier.json new file mode 100644 index 0000000000..2fbc447b30 --- /dev/null +++ b/packages/parser/tests/events/s3EventGlacier.json @@ -0,0 +1,44 @@ +{ + "Records": [ + { + "eventVersion": "2.1", + "eventSource": "aws:s3", + "awsRegion": "us-east-2", + "eventTime": "2019-09-03T19:37:27.192Z", + "eventName": "ObjectCreated:Put", + "userIdentity": { + "principalId": "AWS:AIDAINPONIXQXHT3IKHL2" + }, + "requestParameters": { + "sourceIPAddress": "205.255.255.255" + }, + "responseElements": { + "x-amz-request-id": "D82B88E5F771F645", + "x-amz-id-2": "vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo=" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "828aa6fc-f7b5-4305-8584-487c791949c1", + "bucket": { + "name": "lambda-artifacts-deafc19498e3f2df", + "ownerIdentity": { + "principalId": "A3I5XTEXAMAI3E" + }, + "arn": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df" + }, + "object": { + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "size": 1305107, + "eTag": "b21b84d653bb07b05b1e6b33684dc11b", + "sequencer": "0C0F6F405D6ED209E1" + } + }, + "glacierEventData": { + "restoreEventData": { + "lifecycleRestorationExpiryTime": "1970-01-01T00:01:00.000Z", + "lifecycleRestoreStorageClass": "standard" + } + } + } + ] +} \ No newline at end of file diff --git a/packages/parser/tests/events/s3ObjectEventIAMUser.json b/packages/parser/tests/events/s3ObjectEventIAMUser.json new file mode 100644 index 0000000000..6be41c4352 --- /dev/null +++ b/packages/parser/tests/events/s3ObjectEventIAMUser.json @@ -0,0 +1,30 @@ +{ + "xAmzRequestId": "1a5ed718-5f53-471d-b6fe-5cf62d88d02a", + "getObjectContext": { + "inputS3Url": "https://myap-123412341234.s3-accesspoint.us-east-1.amazonaws.com/s3.txt?X-Amz-Security-Token=...", + "outputRoute": "io-iad-cell001", + "outputToken": "..." + }, + "configuration": { + "accessPointArn": "arn:aws:s3-object-lambda:us-east-1:123412341234:accesspoint/myolap", + "supportingAccessPointArn": "arn:aws:s3:us-east-1:123412341234:accesspoint/myap", + "payload": "test" + }, + "userRequest": { + "url": "/https/github.com/s3.txt", + "headers": { + "Host": "myolap-123412341234.s3-object-lambda.us-east-1.amazonaws.com", + "Accept-Encoding": "identity", + "X-Amz-Content-SHA256": "e3b0c44297fc1c149afbf4c8995fb92427ae41e4649b934ca495991b7852b855" + } + }, + "userIdentity": { + "type": "IAMUser", + "principalId": "...", + "arn": "arn:aws:iam::123412341234:user/myuser", + "accountId": "123412341234", + "accessKeyId": "...", + "userName": "Alice" + }, + "protocolVersion": "1.00" +} diff --git a/packages/parser/tests/events/s3ObjectEventTempCredentials.json b/packages/parser/tests/events/s3ObjectEventTempCredentials.json new file mode 100644 index 0000000000..30c70fe6df --- /dev/null +++ b/packages/parser/tests/events/s3ObjectEventTempCredentials.json @@ -0,0 +1,42 @@ +{ + "xAmzRequestId": "requestId", + "getObjectContext": { + "inputS3Url": "https://my-s3-ap-111122223333.s3-accesspoint.us-east-1.amazonaws.com/example?X-Amz-Security-Token=", + "outputRoute": "io-use1-001", + "outputToken": "OutputToken" + }, + "configuration": { + "accessPointArn": "arn:aws:s3-object-lambda:us-east-1:111122223333:accesspoint/example-object-lambda-ap", + "supportingAccessPointArn": "arn:aws:s3:us-east-1:111122223333:accesspoint/example-ap", + "payload": "{}" + }, + "userRequest": { + "url": "https://object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com/example", + "headers": { + "Host": "object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com", + "Accept-Encoding": "identity", + "X-Amz-Content-SHA256": "e3b0c44298fc1example" + } + }, + "userIdentity": { + "type": "AssumedRole", + "principalId": "principalId", + "arn": "arn:aws:sts::111122223333:assumed-role/Admin/example", + "accountId": "111122223333", + "accessKeyId": "accessKeyId", + "sessionContext": { + "attributes": { + "mfaAuthenticated": "false", + "creationDate": "Wed Mar 10 23:41:52 UTC 2021" + }, + "sessionIssuer": { + "type": "Role", + "principalId": "principalId", + "arn": "arn:aws:iam::111122223333:role/Admin", + "accountId": "111122223333", + "userName": "Admin" + } + } + }, + "protocolVersion": "1.00" +} diff --git a/packages/parser/tests/events/s3SqsEvent.json b/packages/parser/tests/events/s3SqsEvent.json new file mode 100644 index 0000000000..55863af12b --- /dev/null +++ b/packages/parser/tests/events/s3SqsEvent.json @@ -0,0 +1,22 @@ +{ + "Records":[ + { + "messageId":"ca3e7a89-c358-40e5-8aa0-5da01403c267", + "receiptHandle":"AQEBE7XoI7IQRLF7SrpiW9W4BanmOWe8UtVDbv6/CEZYKf/OktSNIb4j689tQfR4k44V/LY20lZ5VpxYt2GTYCsSLKTcBalTJaRX9CKu/hVqy/23sSNiKxnP56D+VLSn+hU275+AP1h4pUL0d9gLdRB2haX8xiM+LcGfis5Jl8BBXtoxKRF60O87O9/NvCmmXLeqkJuexfyEZNyed0fFCRXFXSjbmThG0OIQgcrGI8glBRGPA8htns58VtXFsSaPYNoqP3p5n6+ewKKVLD0lfm+0DlnLKRa+mjvFBaSer9KK1ff+Aq6zJ6HynPwADj+aF70Hwimc2zImYe51SLEF/E2csYlMNZYI/2qXW0m9R7wJ/XDTV4g2+h+BMTxsKnJQ6NQd", + "body":"{\"Records\":[{\"eventVersion\":\"2.1\",\"eventSource\":\"aws:s3\",\"awsRegion\":\"us-east-1\",\"eventTime\":\"2023-04-12T20:43:38.021Z\",\"eventName\":\"ObjectCreated:Put\",\"userIdentity\":{\"principalId\":\"A1YQ72UWCM96UF\"},\"requestParameters\":{\"sourceIPAddress\":\"93.108.161.96\"},\"responseElements\":{\"x-amz-request-id\":\"YMSSR8BZJ2Y99K6P\",\"x-amz-id-2\":\"6ASrUfj5xpn859fIq+6FXflOex/SKl/rjfiMd7wRzMg/zkHKR22PDpnh7KD3uq//cuOTbdX4DInN5eIs+cR0dY1z2Mc5NDP/\"},\"s3\":{\"s3SchemaVersion\":\"1.0\",\"configurationId\":\"SNS\",\"bucket\":{\"name\":\"xxx\",\"ownerIdentity\":{\"principalId\":\"A1YQ72UWCM96UF\"},\"arn\":\"arn:aws:s3:::xxx\"},\"object\":{\"key\":\"test.pdf\",\"size\":104681,\"eTag\":\"2e3ad1e983318bbd8e73b080e2997980\",\"versionId\":\"yd3d4HaWOT2zguDLvIQLU6ptDTwKBnQV\",\"sequencer\":\"00643717F9F8B85354\"}}}]}", + "attributes":{ + "ApproximateReceiveCount":"1", + "SentTimestamp":"1681332219270", + "SenderId":"AIDAJHIPRHEMV73VRJEBU", + "ApproximateFirstReceiveTimestamp":"1681332239270" + }, + "messageAttributes":{ + + }, + "md5OfBody":"16f4460f4477d8d693a5abe94fdbbd73", + "eventSource":"aws:sqs", + "eventSourceARN":"arn:aws:sqs:us-east-1:123456789012:SQS", + "awsRegion":"us-east-1" + } + ] + } diff --git a/packages/parser/tests/events/secretsManagerEvent.json b/packages/parser/tests/events/secretsManagerEvent.json new file mode 100644 index 0000000000..f07ea1e0b0 --- /dev/null +++ b/packages/parser/tests/events/secretsManagerEvent.json @@ -0,0 +1,5 @@ +{ + "SecretId":"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3", + "ClientRequestToken":"550e8400-e29b-41d4-a716-446655440000", + "Step":"createSecret" +} \ No newline at end of file diff --git a/packages/parser/tests/events/sesEvent.json b/packages/parser/tests/events/sesEvent.json new file mode 100644 index 0000000000..636ecad687 --- /dev/null +++ b/packages/parser/tests/events/sesEvent.json @@ -0,0 +1,101 @@ +{ + "Records": [ + { + "eventVersion": "1.0", + "ses": { + "mail": { + "commonHeaders": { + "from": [ + "Jane Doe " + ], + "to": [ + "johndoe@example.com" + ], + "returnPath": "janedoe@example.com", + "messageId": "<0123456789example.com>", + "date": "Wed, 7 Oct 2015 12:34:56 -0700", + "subject": "Test Subject" + }, + "source": "janedoe@example.com", + "timestamp": "1970-01-01T00:00:00.000Z", + "destination": [ + "johndoe@example.com" + ], + "headers": [ + { + "name": "Return-Path", + "value": "" + }, + { + "name": "Received", + "value": "from mailer.example.com (mailer.example.com [203.0.113.1]) by ..." + }, + { + "name": "DKIM-Signature", + "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=example; ..." + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "From", + "value": "Jane Doe " + }, + { + "name": "Date", + "value": "Wed, 7 Oct 2015 12:34:56 -0700" + }, + { + "name": "Message-ID", + "value": "<0123456789example.com>" + }, + { + "name": "Subject", + "value": "Test Subject" + }, + { + "name": "To", + "value": "johndoe@example.com" + }, + { + "name": "Content-Type", + "value": "text/plain; charset=UTF-8" + } + ], + "headersTruncated": false, + "messageId": "o3vrnil0e2ic28tr" + }, + "receipt": { + "recipients": [ + "johndoe@example.com" + ], + "timestamp": "1970-01-01T00:00:00.000Z", + "spamVerdict": { + "status": "PASS" + }, + "dkimVerdict": { + "status": "PASS" + }, + "dmarcPolicy": "reject", + "processingTimeMillis": 574, + "action": { + "type": "Lambda", + "invocationType": "Event", + "functionArn": "arn:aws:lambda:us-west-2:012345678912:function:Example" + }, + "dmarcVerdict": { + "status": "PASS" + }, + "spfVerdict": { + "status": "PASS" + }, + "virusVerdict": { + "status": "PASS" + } + } + }, + "eventSource": "aws:ses" + } + ] +} diff --git a/packages/parser/tests/events/snsEvent.json b/packages/parser/tests/events/snsEvent.json new file mode 100644 index 0000000000..3d8a8ed443 --- /dev/null +++ b/packages/parser/tests/events/snsEvent.json @@ -0,0 +1,31 @@ +{ + "Records": [ + { + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:us-east-2:123456789012:sns-la ...", + "EventSource": "aws:sns", + "Sns": { + "SignatureVersion": "1", + "Timestamp": "2019-01-02T12:45:07.000Z", + "Signature": "tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==", + "SigningCertUrl": "https://sns.us-east-2.amazonaws.com/SimpleNotification", + "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", + "Message": "Hello from SNS!", + "MessageAttributes": { + "Test": { + "Type": "String", + "Value": "TestString" + }, + "TestBinary": { + "Type": "Binary", + "Value": "TestBinary" + } + }, + "Type": "Notification", + "UnsubscribeUrl": "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe", + "TopicArn": "arn:aws:sns:us-east-2:123456789012:sns-lambda", + "Subject": "TestInvoke" + } + } + ] +} \ No newline at end of file diff --git a/packages/parser/tests/events/snsSqsEvent.json b/packages/parser/tests/events/snsSqsEvent.json new file mode 100644 index 0000000000..ee440fc296 --- /dev/null +++ b/packages/parser/tests/events/snsSqsEvent.json @@ -0,0 +1,20 @@ +{ + "Records": [ + { + "messageId": "79406a00-bf15-46ca-978c-22c3613fcb30", + "receiptHandle": "AQEB3fkqlBqq239bMCAHIr5mZkxJYKtxsTTy1lMImmpY7zqpQdfcAE8zFiuRh7X5ciROy24taT2rRXfuJFN/yEUVcQ6d5CIOCEK4htmRJJOHIyGdZPAm2NUUG5nNn2aEzgfzVvrkPBsrCbr7XTzK5s6eUZNH/Nn9AJtHKHpzweRK34Bon9OU/mvyIT7EJbwHPsdhL14NrCp8pLWBiIhkaJkG2G6gPO89dwHtGVUARJL+zP70AuIu/f7QgmPtY2eeE4AVbcUT1qaIlSGHUXxoHq/VMHLd/c4zWl0EXQOo/90DbyCUMejTIKL7N15YfkHoQDHprvMiAr9S75cdMiNOduiHzZLg/qVcv4kxsksKLFMKjwlzmYuQYy2KslVGwoHMd4PD", + "body": "{\n \"Type\" : \"Notification\",\n \"MessageId\" : \"d88d4479-6ec0-54fe-b63f-1cf9df4bb16e\",\n \"TopicArn\" : \"arn:aws:sns:eu-west-1:231436140809:powertools265\",\n \"Message\" : \"{\\\"message\\\": \\\"hello world\\\", \\\"username\\\": \\\"lessa\\\"}\",\n \"Timestamp\" : \"2021-01-19T10:07:07.287Z\",\n \"SignatureVersion\" : \"1\",\n \"Signature\" : \"tEo2i6Lw6/Dr7Jdlulh0sXgnkF0idd3hqs8QZCorQpzkIWVOuu583NT0Gv0epuZD1Bo+tex6NgP5p6415yNVujGHJKnkrA9ztzXaVgFiol8rf8AFGQbmb7RsM9BqATQUJeg9nCTe0jksmWXmjxEFr8XKyyRuQBwSlRTngAvOw8jUnCe1vyYD5xPec1xpfOEGLi5BqSog+6tBtsry3oAtcENX8SV1tVuMpp6D+UrrU8xNT/5D70uRDppkPE3vq+t7rR0fVSdQRdUV9KmQD2bflA1Dyb2y37EzwJOMHDDQ82aOhj/JmPxvEAlV8RkZl6J0HIveraRy9wbNLbI7jpiOCw==\",\n \"SigningCertURL\" : \"https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-010a507c1833636cd94bdb98bd93083a.pem\",\n \"UnsubscribeURL\" : \"https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:231436140809:powertools265:15189ad7-870e-40e5-a7dd-a48898cd9f86\"\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1611050827340", + "SenderId": "AIDAISMY7JYY5F7RTT6AO", + "ApproximateFirstReceiveTimestamp": "1611050827344" + }, + "messageAttributes": {}, + "md5OfBody": "8910bdaaf9a30a607f7891037d4af0b0", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-west-1:231436140809:powertools265", + "awsRegion": "eu-west-1" + } + ] +} diff --git a/packages/parser/tests/events/snsSqsFifoEvent.json b/packages/parser/tests/events/snsSqsFifoEvent.json new file mode 100644 index 0000000000..6c23ef6294 --- /dev/null +++ b/packages/parser/tests/events/snsSqsFifoEvent.json @@ -0,0 +1,23 @@ +{ + "Records": [ + { + "messageId": "69bc4bbd-ed69-4325-a434-85c3b428ceab", + "receiptHandle": "AQEBbfAqjhrgIdW3HGWYPz57mdDatG/dT9LZhRPAsNQ1pJmw495w4esDc8ZSbOwMZuPBol7wtiNWug8U25GpSQDDLY1qv//8/lfmdzXOiprG6xRVeiXSHj0j731rJQ3xo+GPdGjOzjIxI09CrE3HtZ4lpXY9NjjHzP8hdxkCLlbttumc8hDBUR365/Tk+GfV2nNP9qvZtLGEbKCdTm/GYdTSoAr+ML9HnnGrS9T25Md71ASiZMI4DZqptN6g7CYYojFPs1LVM9o1258ferA72zbNoQ==", + "body": "{\n \"Type\" : \"Notification\",\n \"MessageId\" : \"a7c9d2fa-77fa-5184-9de9-89391027cc7d\",\n \"SequenceNumber\" : \"10000000000000004000\",\n \"TopicArn\" : \"arn:aws:sns:eu-west-1:231436140809:Test.fifo\",\n \"Message\" : \"{\\\"message\\\": \\\"hello world\\\", \\\"username\\\": \\\"lessa\\\"}\",\n \"Timestamp\" : \"2022-10-14T13:35:25.419Z\",\n \"UnsubscribeURL\" : \"https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:231436140809:Test.fifo:bb81d3de-a0f9-46e4-b619-d3152a4d545f\"\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1665754525442", + "SequenceNumber": "18873177232222703872", + "MessageGroupId": "powertools-test", + "SenderId": "AIDAWYJAWPFU7SUQGUJC6", + "MessageDeduplicationId": "4e0a0f61eed277a4b9e4c01d5722b07b0725e42fe782102abee5711adfac701f", + "ApproximateFirstReceiveTimestamp": "1665754525442" + }, + "messageAttributes": {}, + "md5OfBody": "f3c788e623445e3feb263e80c1bffc0b", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-west-1:231436140809:Test.fifo", + "awsRegion": "eu-west-1" + } + ] +} \ No newline at end of file diff --git a/packages/parser/tests/events/sqsEvent.json b/packages/parser/tests/events/sqsEvent.json new file mode 100644 index 0000000000..9bc0a394b4 --- /dev/null +++ b/packages/parser/tests/events/sqsEvent.json @@ -0,0 +1,43 @@ +{ + "Records": [ + { + "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", + "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", + "body": "Test message.", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1545082649183", + "SenderId": "AIDAIENQZJOLO23YVJ4VO", + "ApproximateFirstReceiveTimestamp": "1545082649185" + }, + "messageAttributes": { + "testAttr": { + "stringValue": "100", + "binaryValue": "base64Str", + "dataType": "Number" + } + }, + "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", + "awsRegion": "us-east-2" + }, + { + "messageId": "2e1424d4-f796-459a-8184-9c92662be6da", + "receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...", + "body": "{\"message\": \"foo1\"}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1545082650636", + "SenderId": "AIDAIENQZJOLO23YVJ4VO", + "ApproximateFirstReceiveTimestamp": "1545082650649", + "DeadLetterQueueSourceArn": "arn:aws:sqs:us-east-2:123456789012:my-queue-dead" + }, + "messageAttributes": {}, + "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", + "awsRegion": "us-east-2" + } + ] +} diff --git a/packages/parser/tests/events/vpcLatticeEvent.json b/packages/parser/tests/events/vpcLatticeEvent.json new file mode 100644 index 0000000000..936bfb22d1 --- /dev/null +++ b/packages/parser/tests/events/vpcLatticeEvent.json @@ -0,0 +1,15 @@ +{ + "raw_path": "/testpath", + "method": "GET", + "headers": { + "user_agent": "curl/7.64.1", + "x-forwarded-for": "10.213.229.10", + "host": "test-lambda-service-3908sdf9u3u.dkfjd93.vpc-lattice-svcs.us-east-2.on.aws", + "accept": "*/*" + }, + "query_string_parameters": { + "order-id": "1" + }, + "body": "eyJ0ZXN0IjogImV2ZW50In0=", + "is_base64_encoded": true +} diff --git a/packages/parser/tests/events/vpcLatticeEventPathTrailingSlash.json b/packages/parser/tests/events/vpcLatticeEventPathTrailingSlash.json new file mode 100644 index 0000000000..7f6c0cfd9a --- /dev/null +++ b/packages/parser/tests/events/vpcLatticeEventPathTrailingSlash.json @@ -0,0 +1,15 @@ +{ + "raw_path": "/testpath/", + "method": "GET", + "headers": { + "user_agent": "curl/7.64.1", + "x-forwarded-for": "10.213.229.10", + "host": "test-lambda-service-3908sdf9u3u.dkfjd93.vpc-lattice-svcs.us-east-2.on.aws", + "accept": "*/*" + }, + "query_string_parameters": { + "order-id": "1" + }, + "body": "eyJ0ZXN0IjogImV2ZW50In0=", + "is_base64_encoded": true +} diff --git a/packages/parser/tests/events/vpcLatticeEventV2PathTrailingSlash.json b/packages/parser/tests/events/vpcLatticeEventV2PathTrailingSlash.json new file mode 100644 index 0000000000..a9f0188852 --- /dev/null +++ b/packages/parser/tests/events/vpcLatticeEventV2PathTrailingSlash.json @@ -0,0 +1,30 @@ +{ + "version": "2.0", + "path": "/newpath/", + "method": "GET", + "headers": { + "user_agent": "curl/7.64.1", + "x-forwarded-for": "10.213.229.10", + "host": "test-lambda-service-3908sdf9u3u.dkfjd93.vpc-lattice-svcs.us-east-2.on.aws", + "accept": "*/*" + }, + "queryStringParameters": { + "order-id": "1" + }, + "body": "{\"message\": \"Hello from Lambda!\"}", + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:us-east-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:us-east-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:region:123456789012:vpc/vpc-0b8276c84697e7339", + "type" : "AWS_IAM", + "principal": "arn:aws:sts::123456789012:assumed-role/example-role/057d00f8b51257ba3c853a0f248943cf", + "sessionName": "057d00f8b51257ba3c853a0f248943cf", + "x509SanDns": "example.com" + }, + "region": "us-east-2", + "timeEpoch": "1696331543569073" + } +} diff --git a/packages/parser/tests/events/vpcLatticeV2Event.json b/packages/parser/tests/events/vpcLatticeV2Event.json new file mode 100644 index 0000000000..fe10d83a3a --- /dev/null +++ b/packages/parser/tests/events/vpcLatticeV2Event.json @@ -0,0 +1,30 @@ +{ + "version": "2.0", + "path": "/newpath", + "method": "GET", + "headers": { + "user_agent": "curl/7.64.1", + "x-forwarded-for": "10.213.229.10", + "host": "test-lambda-service-3908sdf9u3u.dkfjd93.vpc-lattice-svcs.us-east-2.on.aws", + "accept": "*/*" + }, + "queryStringParameters": { + "order-id": "1" + }, + "body": "{\"message\": \"Hello from Lambda!\"}", + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:us-east-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:us-east-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:region:123456789012:vpc/vpc-0b8276c84697e7339", + "type" : "AWS_IAM", + "principal": "arn:aws:sts::123456789012:assumed-role/example-role/057d00f8b51257ba3c853a0f248943cf", + "sessionName": "057d00f8b51257ba3c853a0f248943cf", + "x509SanDns": "example.com" + }, + "region": "us-east-2", + "timeEpoch": "1696331543569073" + } +} diff --git a/packages/parser/tests/tsconfig.json b/packages/parser/tests/tsconfig.json new file mode 100644 index 0000000000..dc4ebdfc5f --- /dev/null +++ b/packages/parser/tests/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "../", + "noEmit": true + }, + "include": [ + "../src/**/*", + "./**/*.*" + ] +} \ No newline at end of file diff --git a/packages/parser/tests/unit/envelope.test.ts b/packages/parser/tests/unit/envelope.test.ts new file mode 100644 index 0000000000..8d7778bc25 --- /dev/null +++ b/packages/parser/tests/unit/envelope.test.ts @@ -0,0 +1,78 @@ +import { z } from 'zod'; +import { Envelope } from '../../src/envelopes/envelope.js'; +import { ParseError } from '../../src/errors.js'; + +describe('envelope: ', () => { + describe('parseSafe', () => { + it('returns success response when input is object', () => { + const result = Envelope.safeParse( + '{"name": "John"}', + z.object({ name: z.string() }) + ); + expect(result).toEqual({ + success: true, + data: { name: 'John' }, + }); + }); + it('returns success response when input is string', () => { + const result = Envelope.safeParse( + { name: 'John' }, + z.object({ name: z.string() }) + ); + expect(result).toEqual({ + success: true, + data: { name: 'John' }, + }); + }); + it('returns error when input does not match schema', () => { + const result = Envelope.safeParse( + { name: 123 }, + z.object({ name: z.string() }) + ); + expect(result).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: { name: 123 }, + }); + }); + + it('returns error when input is invalid JSON string', () => { + const result = Envelope.safeParse( + '{name: "John"}', + z.object({ name: z.string() }) + ); + expect(result).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: '{name: "John"}', + }); + }); + }); + + describe('parse', () => { + it('returns parsed data when input is object', () => { + const result = Envelope.parse( + { name: 'John' }, + z.object({ name: z.string() }) + ); + expect(result).toEqual({ name: 'John' }); + }); + it('returns parsed data when input is string', () => { + const result = Envelope.parse( + '{"name": "John"}', + z.object({ name: z.string() }) + ); + expect(result).toEqual({ name: 'John' }); + }); + it('throw custom error if input is not string or object', () => { + expect(() => Envelope.parse(123, z.object({ name: z.string() }))).toThrow( + 'Invalid data type for envelope. Expected string or object, got number' + ); + }); + it('throws error when input does not match schema', () => { + expect(() => + Envelope.parse({ name: 123 }, z.object({ name: z.string() })) + ).toThrow(); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/apigwt.test.ts b/packages/parser/tests/unit/envelopes/apigwt.test.ts new file mode 100644 index 0000000000..0a1e675ae4 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/apigwt.test.ts @@ -0,0 +1,92 @@ +/** + * Test built in schema envelopes for api gateway + * + * @group unit/parser/envelopes + */ + +import { generateMock } from '@anatine/zod-mock'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { APIGatewayProxyEvent } from '../../../src/types/'; +import { ApiGatewayEnvelope } from '../../../src/envelopes/index.js'; +import { ParseError } from '../../../src/errors.js'; + +describe('ApigwEnvelope ', () => { + describe('parse', () => { + it('should parse custom schema in envelope', () => { + const testCustomSchemaObject = generateMock(TestSchema); + const testEvent = TestEvents.apiGatewayProxyEvent as APIGatewayProxyEvent; + + testEvent.body = JSON.stringify(testCustomSchemaObject); + + const resp = ApiGatewayEnvelope.parse(testEvent, TestSchema); + expect(resp).toEqual(testCustomSchemaObject); + }); + + it('should throw no body provided', () => { + const testEvent = TestEvents.apiGatewayProxyEvent as APIGatewayProxyEvent; + testEvent.body = undefined; + + expect(() => ApiGatewayEnvelope.parse(testEvent, TestSchema)).toThrow( + ParseError + ); + }); + it('should throw invalid event provided', () => { + const testEvent = TestEvents.apiGatewayProxyEvent as APIGatewayProxyEvent; + testEvent.body = 'invalid'; + + expect(() => ApiGatewayEnvelope.parse(testEvent, TestSchema)).toThrow( + ParseError + ); + }); + }); + + describe('safeParse', () => { + it('should parse custom schema in envelope', () => { + const testCustomSchemaObject = generateMock(TestSchema); + const testEvent = TestEvents.apiGatewayProxyEvent as APIGatewayProxyEvent; + + testEvent.body = JSON.stringify(testCustomSchemaObject); + + const resp = ApiGatewayEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ + success: true, + data: testCustomSchemaObject, + }); + }); + + it('should return success false with original body if no body provided', () => { + const testEvent = TestEvents.apiGatewayProxyEvent as APIGatewayProxyEvent; + testEvent.body = undefined; + + const resp = ApiGatewayEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); + + it('should return success false with original body if invalid body provided', () => { + const testEvent = TestEvents.apiGatewayProxyEvent as APIGatewayProxyEvent; + testEvent.body = 'invalid'; + + const resp = ApiGatewayEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: testEvent, + }); + }); + it('should return success false if event is invalid', () => { + const resp = ApiGatewayEnvelope.safeParse( + 'invalid' as unknown, + TestSchema + ); + expect(resp).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: 'invalid', + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/apigwv2.test.ts b/packages/parser/tests/unit/envelopes/apigwv2.test.ts new file mode 100644 index 0000000000..dd5a82609b --- /dev/null +++ b/packages/parser/tests/unit/envelopes/apigwv2.test.ts @@ -0,0 +1,94 @@ +/** + * Test built in schema envelopes for api gateway v2 + * + * @group unit/parser/envelopes + */ + +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { generateMock } from '@anatine/zod-mock'; +import { APIGatewayProxyEventV2 } from 'aws-lambda'; +import { ApiGatewayV2Envelope } from '../../../src/envelopes/index.js'; + +describe('ApiGwV2Envelope ', () => { + describe('parse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + const data = generateMock(TestSchema); + + testEvent.body = JSON.stringify(data); + + expect(ApiGatewayV2Envelope.parse(testEvent, TestSchema)).toEqual(data); + }); + + it('should throw when no body provided', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + testEvent.body = undefined; + + expect(() => ApiGatewayV2Envelope.parse(testEvent, TestSchema)).toThrow(); + }); + + it('should throw when invalid body provided', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + testEvent.body = 'invalid'; + + expect(() => ApiGatewayV2Envelope.parse(testEvent, TestSchema)).toThrow(); + }); + it('should throw when invalid event provided', () => { + expect(() => + ApiGatewayV2Envelope.parse({ foo: 'bar' }, TestSchema) + ).toThrow(); + }); + }); + + describe('safeParse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + const data = generateMock(TestSchema); + + testEvent.body = JSON.stringify(data); + + expect(ApiGatewayV2Envelope.safeParse(testEvent, TestSchema)).toEqual({ + success: true, + data, + }); + }); + + it('should return success false with original body if no body provided', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + testEvent.body = undefined; + + expect(ApiGatewayV2Envelope.safeParse(testEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); + + it('should return success false with original body if invalid body provided', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + testEvent.body = 'invalid'; + + expect(ApiGatewayV2Envelope.safeParse(testEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); + + it('should return success false with original event if invalid event provided', () => { + expect( + ApiGatewayV2Envelope.safeParse({ foo: 'bar' }, TestSchema) + ).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/cloudwatch.test.ts b/packages/parser/tests/unit/envelopes/cloudwatch.test.ts new file mode 100644 index 0000000000..6eec52fd9a --- /dev/null +++ b/packages/parser/tests/unit/envelopes/cloudwatch.test.ts @@ -0,0 +1,131 @@ +/** + * Test built in schema envelopes for CloudWatch + * + * @group unit/parser/envelopes + */ + +import { generateMock } from '@anatine/zod-mock'; +import { gzipSync } from 'node:zlib'; +import { + CloudWatchLogEventSchema, + CloudWatchLogsDecodeSchema, +} from '../../../src/schemas/'; +import { TestSchema } from '../schema/utils.js'; +import { CloudWatchEnvelope } from '../../../src/envelopes/index.js'; +import { ParseError } from '../../../src'; + +describe('CloudWatch', () => { + describe('parse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = { + awslogs: { + data: '', + }, + }; + + const data = generateMock(TestSchema); + const eventMock = generateMock(CloudWatchLogEventSchema, { + stringMap: { + message: () => JSON.stringify(data), + }, + }); + + const logMock = generateMock(CloudWatchLogsDecodeSchema); + logMock.logEvents = [eventMock]; + + testEvent.awslogs.data = gzipSync( + Buffer.from(JSON.stringify(logMock), 'utf8') + ).toString('base64'); + + expect(CloudWatchEnvelope.parse(testEvent, TestSchema)).toEqual([data]); + }); + + it('should throw when schema does not match', () => { + const testEvent = { + awslogs: { + data: '', + }, + }; + + const eventMock = generateMock(CloudWatchLogEventSchema, { + stringMap: { + message: () => JSON.stringify({ foo: 'bar' }), + }, + }); + + const logMock = generateMock(CloudWatchLogsDecodeSchema); + logMock.logEvents = [eventMock]; + + testEvent.awslogs.data = gzipSync( + Buffer.from(JSON.stringify(logMock), 'utf8') + ).toString('base64'); + + expect(() => CloudWatchEnvelope.parse(testEvent, TestSchema)).toThrow(); + }); + }); + + describe('safeParse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = { + awslogs: { + data: '', + }, + }; + + const data = generateMock(TestSchema); + const eventMock = generateMock(CloudWatchLogEventSchema, { + stringMap: { + message: () => JSON.stringify(data), + }, + }); + + const logMock = generateMock(CloudWatchLogsDecodeSchema); + logMock.logEvents = [eventMock]; + + testEvent.awslogs.data = gzipSync( + Buffer.from(JSON.stringify(logMock), 'utf8') + ).toString('base64'); + + const actual = CloudWatchEnvelope.safeParse(testEvent, TestSchema); + expect(actual).toEqual({ + success: true, + data: [data], + }); + }); + + it('should return success false when schema does not match', () => { + const testEvent = { + awslogs: { + data: '', + }, + }; + + const eventMock = generateMock(CloudWatchLogEventSchema, { + stringMap: { + message: () => JSON.stringify({ foo: 'bar' }), + }, + }); + + const logMock = generateMock(CloudWatchLogsDecodeSchema); + logMock.logEvents = [eventMock]; + + testEvent.awslogs.data = gzipSync( + Buffer.from(JSON.stringify(logMock), 'utf8') + ).toString('base64'); + + expect(CloudWatchEnvelope.safeParse(testEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); + + it('should return success false when envelope does not match', () => { + expect(CloudWatchEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: { foo: 'bar' }, + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/dynamodb.test.ts b/packages/parser/tests/unit/envelopes/dynamodb.test.ts new file mode 100644 index 0000000000..80385eca45 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/dynamodb.test.ts @@ -0,0 +1,129 @@ +/** + * Test built in schema envelopes for api gateway v2 + * + * @group unit/parser/envelopes + */ + +import { generateMock } from '@anatine/zod-mock'; +import { TestEvents } from '../schema/utils.js'; +import { DynamoDBStreamEvent } from 'aws-lambda'; +import { z } from 'zod'; +import { DynamoDBStreamEnvelope } from '../../../src/envelopes/index.js'; +import { ParseError } from '../../../src/errors.js'; + +describe('DynamoDB', () => { + const schema = z.object({ + Message: z.record(z.literal('S'), z.string()), + Id: z.record(z.literal('N'), z.number().min(0).max(100)), + }); + const mockOldImage = generateMock(schema); + const mockNewImage = generateMock(schema); + const dynamodbEvent = TestEvents.dynamoStreamEvent as DynamoDBStreamEvent; + (dynamodbEvent.Records[0].dynamodb!.NewImage as typeof mockNewImage) = + mockNewImage; + (dynamodbEvent.Records[1].dynamodb!.NewImage as typeof mockNewImage) = + mockNewImage; + (dynamodbEvent.Records[0].dynamodb!.OldImage as typeof mockOldImage) = + mockOldImage; + (dynamodbEvent.Records[1].dynamodb!.OldImage as typeof mockOldImage) = + mockOldImage; + describe('parse', () => { + it('parse should parse dynamodb envelope', () => { + const parsed = DynamoDBStreamEnvelope.parse(dynamodbEvent, schema); + expect(parsed[0]).toEqual({ + OldImage: mockOldImage, + NewImage: mockNewImage, + }); + expect(parsed[1]).toEqual({ + OldImage: mockOldImage, + NewImage: mockNewImage, + }); + }); + it('parse should throw error if envelope invalid', () => { + expect(() => + DynamoDBStreamEnvelope.parse({ foo: 'bar' }, schema) + ).toThrow(); + }); + it('parse should throw error if new or old image is invalid', () => { + const ddbEvent = TestEvents.dynamoStreamEvent as DynamoDBStreamEvent; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ddbEvent.Records[0].dynamodb!.NewImage.Id = 'foo'; + expect(() => DynamoDBStreamEnvelope.parse(ddbEvent, schema)).toThrow(); + }); + }); + + describe('safeParse', () => { + it('safeParse should parse dynamodb envelope', () => { + const parsed = DynamoDBStreamEnvelope.safeParse(dynamodbEvent, schema); + expect(parsed.success).toBe(true); + expect(parsed).toEqual({ + success: true, + data: [ + { + OldImage: mockOldImage, + NewImage: mockNewImage, + }, + { + OldImage: mockOldImage, + NewImage: mockNewImage, + }, + ], + }); + }); + it('safeParse should return error if NewImage is invalid', () => { + const invalidDDBEvent = + TestEvents.dynamoStreamEvent as DynamoDBStreamEvent; + + (invalidDDBEvent.Records[0].dynamodb!.NewImage as typeof mockNewImage) = { + Id: { N: 101 }, + Message: { S: 'foo' }, + }; + (invalidDDBEvent.Records[1].dynamodb!.NewImage as typeof mockNewImage) = + mockNewImage; + (invalidDDBEvent.Records[0].dynamodb!.OldImage as typeof mockOldImage) = + mockOldImage; + (invalidDDBEvent.Records[1].dynamodb!.OldImage as typeof mockOldImage) = + mockOldImage; + + const parsed = DynamoDBStreamEnvelope.safeParse(invalidDDBEvent, schema); + expect(parsed).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: invalidDDBEvent, + }); + }); + + it('safeParse should return error if OldImage is invalid', () => { + const invalidDDBEvent = + TestEvents.dynamoStreamEvent as DynamoDBStreamEvent; + + (invalidDDBEvent.Records[0].dynamodb!.OldImage as typeof mockNewImage) = { + Id: { N: 101 }, + Message: { S: 'foo' }, + }; + (invalidDDBEvent.Records[1].dynamodb!.NewImage as typeof mockNewImage) = + mockNewImage; + (invalidDDBEvent.Records[0].dynamodb!.OldImage as typeof mockOldImage) = + mockOldImage; + (invalidDDBEvent.Records[0].dynamodb!.NewImage as typeof mockNewImage) = + mockNewImage; + + const parsed = DynamoDBStreamEnvelope.safeParse(invalidDDBEvent, schema); + expect(parsed).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: invalidDDBEvent, + }); + }); + + it('safeParse should return error if envelope is invalid', () => { + const parsed = DynamoDBStreamEnvelope.safeParse({ foo: 'bar' }, schema); + expect(parsed).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: { foo: 'bar' }, + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/eventbridge.test.ts b/packages/parser/tests/unit/envelopes/eventbridge.test.ts new file mode 100644 index 0000000000..1c57549818 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/eventbridge.test.ts @@ -0,0 +1,122 @@ +/** + * Test built in schema envelopes for event bridge + * + * @group unit/parser/envelopes + */ + +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { generateMock } from '@anatine/zod-mock'; +import { EventBridgeEvent } from 'aws-lambda'; +import { EventBridgeEnvelope } from '../../../src/envelopes/index.js'; +import { ParseError } from '../../../src/errors.js'; + +describe('EventBridgeEnvelope ', () => { + describe('parse', () => { + it('should parse eventbridge event', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; + + const data = generateMock(TestSchema); + + eventBridgeEvent.detail = data; + + expect(EventBridgeEnvelope.parse(eventBridgeEvent, TestSchema)).toEqual( + data + ); + }); + + it('should throw error if detail type does not match schema', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; + + eventBridgeEvent.detail = { + foo: 'bar', + }; + + expect(() => + EventBridgeEnvelope.parse(eventBridgeEvent, TestSchema) + ).toThrowError(); + }); + + it('should throw when invalid data type provided', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; + + eventBridgeEvent.detail = 1 as unknown as object; + + expect(() => + EventBridgeEnvelope.parse(eventBridgeEvent, TestSchema) + ).toThrow(); + }); + }); + + describe('safeParse', () => { + it('should safe parse eventbridge event', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; + + const data = generateMock(TestSchema); + + eventBridgeEvent.detail = data; + + expect( + EventBridgeEnvelope.safeParse(eventBridgeEvent, TestSchema) + ).toEqual({ + success: true, + data: data, + }); + }); + + it('should safe parse eventbridge event and return original event if invalid', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; + + eventBridgeEvent.detail = { + foo: 'bar', + }; + + expect( + EventBridgeEnvelope.safeParse(eventBridgeEvent, TestSchema) + ).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: eventBridgeEvent, + }); + }); + + it('should safe parse eventbridge event and return original event if invalid data type', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; + + eventBridgeEvent.detail = 1 as unknown as object; + + expect( + EventBridgeEnvelope.safeParse(eventBridgeEvent, TestSchema) + ).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: eventBridgeEvent, + }); + }); + + it('should return original event and error envelope is invalid', () => { + expect(EventBridgeEnvelope.safeParse(1, TestSchema)).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: 1, + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/kafka.test.ts b/packages/parser/tests/unit/envelopes/kafka.test.ts new file mode 100644 index 0000000000..5c55013244 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/kafka.test.ts @@ -0,0 +1,96 @@ +/** + * Test built in schema envelopes for api gateway v2 + * + * @group unit/parser/envelopes + */ + +import { generateMock } from '@anatine/zod-mock'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { MSKEvent, SelfManagedKafkaEvent } from 'aws-lambda'; +import { KafkaEnvelope } from '../../../src/envelopes/index.js'; + +describe('Kafka', () => { + describe('parse', () => { + it('should parse MSK kafka envelope', () => { + const mock = generateMock(TestSchema); + + const kafkaEvent = TestEvents.kafkaEventMsk as MSKEvent; + kafkaEvent.records['mytopic-0'][0].value = Buffer.from( + JSON.stringify(mock) + ).toString('base64'); + + const result = KafkaEnvelope.parse(kafkaEvent, TestSchema); + + expect(result).toEqual([[mock]]); + }); + + it('should parse Self Managed kafka envelope', () => { + const mock = generateMock(TestSchema); + + const kafkaEvent = + TestEvents.kafkaEventSelfManaged as SelfManagedKafkaEvent; + kafkaEvent.records['mytopic-0'][0].value = Buffer.from( + JSON.stringify(mock) + ).toString('base64'); + + const result = KafkaEnvelope.parse(kafkaEvent, TestSchema); + + expect(result).toEqual([[mock]]); + }); + + describe('safeParse', () => { + it('should parse MSK kafka envelope', () => { + const mock = generateMock(TestSchema); + + const kafkaEvent = TestEvents.kafkaEventMsk as MSKEvent; + kafkaEvent.records['mytopic-0'][0].value = Buffer.from( + JSON.stringify(mock) + ).toString('base64'); + + const result = KafkaEnvelope.safeParse(kafkaEvent, TestSchema); + + expect(result).toEqual({ + success: true, + data: [mock], + }); + }); + + it('should parse Self Managed kafka envelope', () => { + const mock = generateMock(TestSchema); + + const kafkaEvent = + TestEvents.kafkaEventSelfManaged as SelfManagedKafkaEvent; + kafkaEvent.records['mytopic-0'][0].value = Buffer.from( + JSON.stringify(mock) + ).toString('base64'); + + const result = KafkaEnvelope.safeParse(kafkaEvent, TestSchema); + + expect(result).toEqual({ + success: true, + data: [mock], + }); + }); + + it('should return original event on failure', () => { + const kafkaEvent = TestEvents.kafkaEventMsk as MSKEvent; + kafkaEvent.records['mytopic-0'][0].value = 'not a valid json'; + + const result = KafkaEnvelope.safeParse(kafkaEvent, TestSchema); + + expect(result).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: kafkaEvent, + }); + }); + it('should return original event and error if envelope is invalid', () => { + expect(KafkaEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts b/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts new file mode 100644 index 0000000000..50951d7c0c --- /dev/null +++ b/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts @@ -0,0 +1,164 @@ +/** + * Test built in schema envelopes for Kinesis Firehose + * + * @group unit/parser/envelopes + */ + +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { generateMock } from '@anatine/zod-mock'; +import { KinesisFirehoseSchema } from '../../../src/schemas/'; +import { z } from 'zod'; +import { KinesisFirehoseEnvelope } from '../../../src/envelopes/index.js'; + +describe('Kinesis Firehose Envelope', () => { + describe('parse', () => { + it('should parse records for PutEvent', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehosePutEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); + + const resp = KinesisFirehoseEnvelope.parse(testEvent, TestSchema); + expect(resp).toEqual([mock, mock]); + }); + + it('should parse a single record for SQS event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehoseSQSEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); + + const resp = KinesisFirehoseEnvelope.parse(testEvent, TestSchema); + expect(resp).toEqual([mock]); + }); + + it('should parse records for kinesis event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehoseKinesisEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); + + const resp = KinesisFirehoseEnvelope.parse(testEvent, TestSchema); + expect(resp).toEqual([mock, mock]); + }); + it('should throw if record is not base64 encoded', () => { + const testEvent = TestEvents.kinesisFirehosePutEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = 'not base64 encoded'; + }); + + expect(() => { + KinesisFirehoseEnvelope.parse(testEvent, TestSchema); + }).toThrow(); + }); + it('should throw if envelope is invalid', () => { + expect(() => { + KinesisFirehoseEnvelope.parse({ foo: 'bar' }, TestSchema); + }).toThrow(); + }); + it('should throw when schema does not match record', () => { + const testEvent = TestEvents.kinesisFirehosePutEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = Buffer.from('not a valid json').toString('base64'); + }); + + expect(() => { + KinesisFirehoseEnvelope.parse(testEvent, TestSchema); + }).toThrow(); + }); + }); + describe('safeParse', () => { + it('should parse records for PutEvent', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehosePutEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); + + const resp = KinesisFirehoseEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ success: true, data: [mock, mock] }); + }); + + it('should parse a single record for SQS event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehoseSQSEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); + + const resp = KinesisFirehoseEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ success: true, data: [mock] }); + }); + + it('should parse records for kinesis event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehoseKinesisEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); + + const resp = KinesisFirehoseEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ success: true, data: [mock, mock] }); + }); + it('should return original event if envelope is invalid', () => { + expect( + KinesisFirehoseEnvelope.safeParse({ foo: 'bar' }, TestSchema) + ).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); + it('should return original event if record is not base64 encoded', () => { + const testEvent = TestEvents.kinesisFirehosePutEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = 'not base64 encoded'; + }); + + expect(KinesisFirehoseEnvelope.safeParse(testEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); + it('should return original event envelope is invalid', () => { + expect( + KinesisFirehoseEnvelope.safeParse({ foo: 'bar' }, TestSchema) + ).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/kinesis.test.ts b/packages/parser/tests/unit/envelopes/kinesis.test.ts new file mode 100644 index 0000000000..b30c0f1e75 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/kinesis.test.ts @@ -0,0 +1,72 @@ +/** + * Test built in schema envelopes for Kinesis + * + * @group unit/parser/envelopes + */ + +import { generateMock } from '@anatine/zod-mock'; +import { KinesisStreamEvent } from 'aws-lambda'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { KinesisEnvelope } from '../../../src/envelopes/index.js'; +import { ParseError } from '../../../src/errors.js'; + +describe('KinesisEnvelope', () => { + describe('parse', () => { + it('should parse Kinesis Stream event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisStreamEvent as KinesisStreamEvent; + + testEvent.Records.map((record) => { + record.kinesis.data = Buffer.from(JSON.stringify(mock)).toString( + 'base64' + ); + }); + + const resp = KinesisEnvelope.parse(testEvent, TestSchema); + expect(resp).toEqual([mock, mock]); + }); + it('should throw if envelope is invalid', () => { + expect(() => KinesisEnvelope.parse({ foo: 'bar' }, TestSchema)).toThrow(); + }); + it('should throw if record is invalid', () => { + const testEvent = TestEvents.kinesisStreamEvent as KinesisStreamEvent; + testEvent.Records[0].kinesis.data = 'invalid'; + expect(() => KinesisEnvelope.parse(testEvent, TestSchema)).toThrow(); + }); + }); + + describe('safeParse', () => { + it('should parse Kinesis Stream event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisStreamEvent as KinesisStreamEvent; + + testEvent.Records.map((record) => { + record.kinesis.data = Buffer.from(JSON.stringify(mock)).toString( + 'base64' + ); + }); + + const resp = KinesisEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ success: true, data: [mock, mock] }); + }); + it('should return original event if envelope is invalid', () => { + const testEvent = { foo: 'bar' }; + const resp = KinesisEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: testEvent, + }); + }); + it('should return original event if record is invalid', () => { + const testEvent = TestEvents.kinesisStreamEvent as KinesisStreamEvent; + testEvent.Records[0].kinesis.data = 'invalid'; + const resp = KinesisEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: testEvent, + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/lambda.test.ts b/packages/parser/tests/unit/envelopes/lambda.test.ts new file mode 100644 index 0000000000..56b0551cfd --- /dev/null +++ b/packages/parser/tests/unit/envelopes/lambda.test.ts @@ -0,0 +1,92 @@ +import { LambdaFunctionUrlEnvelope } from '../../../src/envelopes/index.js'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { generateMock } from '@anatine/zod-mock'; +import { APIGatewayProxyEventV2 } from 'aws-lambda'; + +/** + * Test built in schema envelopes for Lambda Functions URL + * + * @group unit/parser/envelopes + */ + +describe('Lambda Functions Url ', () => { + describe('parse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = + TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; + const data = generateMock(TestSchema); + + testEvent.body = JSON.stringify(data); + + expect(LambdaFunctionUrlEnvelope.parse(testEvent, TestSchema)).toEqual( + data + ); + }); + + it('should throw when no body provided', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + testEvent.body = undefined; + + expect(() => + LambdaFunctionUrlEnvelope.parse(testEvent, TestSchema) + ).toThrow(); + }); + + it('should throw when envelope is not valid', () => { + expect(() => + LambdaFunctionUrlEnvelope.parse({ foo: 'bar' }, TestSchema) + ).toThrow(); + }); + + it('should throw when body does not match schema', () => { + const testEvent = + TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; + testEvent.body = JSON.stringify({ foo: 'bar' }); + + expect(() => + LambdaFunctionUrlEnvelope.parse(testEvent, TestSchema) + ).toThrow(); + }); + }); + describe('safeParse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = + TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; + const data = generateMock(TestSchema); + + testEvent.body = JSON.stringify(data); + + expect( + LambdaFunctionUrlEnvelope.safeParse(testEvent, TestSchema) + ).toEqual({ + success: true, + data, + }); + }); + + it('should return original event when envelope is not valid', () => { + expect( + LambdaFunctionUrlEnvelope.safeParse({ foo: 'bar' }, TestSchema) + ).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); + + it('should return original event when body does not match schema', () => { + const testEvent = + TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; + testEvent.body = JSON.stringify({ foo: 'bar' }); + + expect( + LambdaFunctionUrlEnvelope.safeParse(testEvent, TestSchema) + ).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/sns.test.ts b/packages/parser/tests/unit/envelopes/sns.test.ts new file mode 100644 index 0000000000..5a6389c9fe --- /dev/null +++ b/packages/parser/tests/unit/envelopes/sns.test.ts @@ -0,0 +1,164 @@ +/** + * Test built in schema envelopes for SNS + * + * @group unit/parser/envelopes + */ + +import { z } from 'zod'; +import { generateMock } from '@anatine/zod-mock'; +import { SNSEvent, SQSEvent } from 'aws-lambda'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { SnsEnvelope, SnsSqsEnvelope } from '../../../src/envelopes/index.js'; +import { ParseError } from '../../../src/errors.js'; + +describe('Sns and SQS Envelope', () => { + describe('SnsSqsEnvelope parse', () => { + it('should parse sqs inside sns envelope', () => { + const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; + + const data = generateMock(TestSchema); + const snsEvent = JSON.parse(snsSqsTestEvent.Records[0].body); + snsEvent.Message = JSON.stringify(data); + + snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); + + expect(SnsSqsEnvelope.parse(snsSqsTestEvent, TestSchema)).toEqual([data]); + }); + }); + describe('SnsSqsEnvelope safeParse', () => { + it('should parse sqs inside sns envelope', () => { + const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; + + const data = generateMock(TestSchema); + const snsEvent = JSON.parse(snsSqsTestEvent.Records[0].body); + snsEvent.Message = JSON.stringify(data); + + snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); + + expect(SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ + success: true, + data: [data], + }); + }); + it('should return error when envelope is not valid', () => { + expect(SnsSqsEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); + it('should return error if message does not match schema', () => { + const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; + + const snsEvent = JSON.parse(snsSqsTestEvent.Records[0].body); + snsEvent.Message = JSON.stringify({ + foo: 'bar', + }); + + snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); + + expect(SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: snsSqsTestEvent, + }); + }); + it('should return error if sns message is not valid', () => { + const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; + + snsSqsTestEvent.Records[0].body = JSON.stringify({ + foo: 'bar', + }); + + expect(SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: snsSqsTestEvent, + }); + }); + it('should return error if JSON parse fails for record.body', () => { + const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; + + snsSqsTestEvent.Records[0].body = 'not a json string'; + + expect(SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: snsSqsTestEvent, + }); + }); + }); +}); +describe('SnsEnvelope', () => { + describe('parse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = TestEvents.snsEvent as SNSEvent; + + const testRecords = [] as z.infer[]; + + testEvent.Records.map((record) => { + const value = generateMock(TestSchema); + testRecords.push(value); + record.Sns.Message = JSON.stringify(value); + }); + + expect(SnsEnvelope.parse(testEvent, TestSchema)).toEqual(testRecords); + }); + + it('should throw if message does not macht schema', () => { + const testEvent = TestEvents.snsEvent as SNSEvent; + + testEvent.Records.map((record) => { + record.Sns.Message = JSON.stringify({ + foo: 'bar', + }); + }); + + expect(() => SnsEnvelope.parse(testEvent, TestSchema)).toThrow(); + }); + it('should throw if envelope is not valid', () => { + expect(() => SnsEnvelope.parse({ foo: 'bar' }, TestSchema)).toThrow(); + }); + }); + describe('safeParse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = TestEvents.snsEvent as SNSEvent; + + const testRecords = [] as z.infer[]; + + testEvent.Records.map((record) => { + const value = generateMock(TestSchema); + testRecords.push(value); + record.Sns.Message = JSON.stringify(value); + }); + + expect(SnsEnvelope.safeParse(testEvent, TestSchema)).toEqual({ + success: true, + data: testRecords, + }); + }); + + it('should return error when message does not macht schema', () => { + const testEvent = TestEvents.snsEvent as SNSEvent; + + testEvent.Records.map((record) => { + record.Sns.Message = JSON.stringify({ + foo: 'bar', + }); + }); + + expect(SnsEnvelope.safeParse(testEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: testEvent, + }); + }); + it('should return error when envelope is not valid', () => { + expect(SnsEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/sqs.test.ts b/packages/parser/tests/unit/envelopes/sqs.test.ts new file mode 100644 index 0000000000..778e0baee1 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/sqs.test.ts @@ -0,0 +1,70 @@ +/** + * Test built in schema envelopes for sqs + * + * @group unit/parser/envelopes + */ + +import { generateMock } from '@anatine/zod-mock'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { SQSEvent } from 'aws-lambda'; +import { SqsEnvelope } from '../../../src/envelopes/sqs.js'; +import { ParseError } from '../../../src/errors.js'; + +describe('SqsEnvelope ', () => { + describe('parse', () => { + it('should parse custom schema in envelope', () => { + const mock = generateMock(TestSchema); + + const sqsEvent = TestEvents.sqsEvent as SQSEvent; + sqsEvent.Records[0].body = JSON.stringify(mock); + sqsEvent.Records[1].body = JSON.stringify(mock); + + const resp = SqsEnvelope.parse(sqsEvent, TestSchema); + expect(resp).toEqual([mock, mock]); + }); + + it('should throw error if invalid keys for a schema', () => { + expect(() => { + SqsEnvelope.parse({ Records: [{ foo: 'bar' }] }, TestSchema); + }).toThrow(); + }); + + it('should throw if invalid envelope', () => { + expect(() => { + SqsEnvelope.parse({ foo: 'bar' }, TestSchema); + }).toThrow(); + }); + }); + describe('safeParse', () => { + it('should parse custom schema in envelope', () => { + const mock = generateMock(TestSchema); + + const sqsEvent = TestEvents.sqsEvent as SQSEvent; + sqsEvent.Records[0].body = JSON.stringify(mock); + sqsEvent.Records[1].body = JSON.stringify(mock); + + expect(SqsEnvelope.safeParse(sqsEvent, TestSchema)).toEqual({ + success: true, + data: [mock, mock], + }); + }); + + it('should return error if event does not match schema', () => { + const sqsEvent = TestEvents.sqsEvent as SQSEvent; + sqsEvent.Records[0].body = JSON.stringify({ foo: 'bar' }); + expect(SqsEnvelope.safeParse(sqsEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: sqsEvent, + }); + }); + + it('should return error if envelope is invalid', () => { + expect(SqsEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: { foo: 'bar' }, + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts b/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts new file mode 100644 index 0000000000..a4282064d2 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts @@ -0,0 +1,97 @@ +/** + * Test built in schema envelopes for VPC Lattice + * + * @group unit/parser/envelopes + */ + +import { generateMock } from '@anatine/zod-mock'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { VpcLatticeEnvelope } from '../../../src/envelopes/index.js'; +import { VpcLatticeEvent } from '../../../src/types/index.js'; + +describe('VpcLatticeEnvelope', () => { + describe('parse', () => { + it('should parse VPC Lattice event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.vpcLatticeEvent as VpcLatticeEvent; + + testEvent.body = JSON.stringify(mock); + + const resp = VpcLatticeEnvelope.parse(testEvent, TestSchema); + + expect(resp).toEqual(mock); + }); + + it('should parse VPC Lattice event with trailing slash', () => { + const mock = generateMock(TestSchema); + const testEvent = + TestEvents.vpcLatticeEventPathTrailingSlash as VpcLatticeEvent; + + testEvent.body = JSON.stringify(mock); + + const resp = VpcLatticeEnvelope.parse(testEvent, TestSchema); + expect(resp).toEqual(mock); + }); + + it('should throw if event is not a VPC Lattice event', () => { + expect(() => + VpcLatticeEnvelope.parse({ foo: 'bar' }, TestSchema) + ).toThrow(); + }); + + it('should throw if body does not match schema', () => { + const testEvent = TestEvents.vpcLatticeEvent as VpcLatticeEvent; + + testEvent.body = JSON.stringify({ foo: 'bar' }); + + expect(() => VpcLatticeEnvelope.parse(testEvent, TestSchema)).toThrow(); + }); + }); + + describe('safeParse', () => { + it('should parse VPC Lattice event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.vpcLatticeEvent as VpcLatticeEvent; + + testEvent.body = JSON.stringify(mock); + + const resp = VpcLatticeEnvelope.safeParse(testEvent, TestSchema); + + expect(resp).toEqual({ success: true, data: mock }); + }); + + it('should parse VPC Lattice event with trailing slash', () => { + const mock = generateMock(TestSchema); + const testEvent = + TestEvents.vpcLatticeEventPathTrailingSlash as VpcLatticeEvent; + + testEvent.body = JSON.stringify(mock); + + const resp = VpcLatticeEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ success: true, data: mock }); + }); + + it('should return error if event is not a VPC Lattice event', () => { + const resp = VpcLatticeEnvelope.safeParse({ foo: 'bar' }, TestSchema); + + expect(resp).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); + + it('should return error if body does not match schema', () => { + const testEvent = TestEvents.vpcLatticeEvent as VpcLatticeEvent; + + testEvent.body = JSON.stringify({ foo: 'bar' }); + + const resp = VpcLatticeEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts b/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts new file mode 100644 index 0000000000..6f615178ec --- /dev/null +++ b/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts @@ -0,0 +1,97 @@ +/** + * Test built in schema envelopes for VPC Lattice V2 + * + * @group unit/parser/envelopes + */ + +import { generateMock } from '@anatine/zod-mock'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { VpcLatticeV2Envelope } from '../../../src/envelopes/index.js'; +import { VpcLatticeEventV2 } from '../../../src/types/index.js'; + +describe('VpcLatticeV2Envelope2', () => { + describe('parse', () => { + it('should parse VPC Lattice event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.vpcLatticeV2Event as VpcLatticeEventV2; + + testEvent.body = JSON.stringify(mock); + + const resp = VpcLatticeV2Envelope.parse(testEvent, TestSchema); + + expect(resp).toEqual(mock); + }); + + it('should parse VPC Lattice event with trailing slash', () => { + const mock = generateMock(TestSchema); + const testEvent = + TestEvents.vpcLatticeEventV2PathTrailingSlash as VpcLatticeEventV2; + + testEvent.body = JSON.stringify(mock); + + const resp = VpcLatticeV2Envelope.parse(testEvent, TestSchema); + expect(resp).toEqual(mock); + }); + + it('should throw if event is not a VPC Lattice event', () => { + expect(() => + VpcLatticeV2Envelope.parse({ foo: 'bar' }, TestSchema) + ).toThrow(); + }); + + it('should throw if body does not match schema', () => { + const testEvent = TestEvents.vpcLatticeV2Event as VpcLatticeEventV2; + + testEvent.body = JSON.stringify({ foo: 'bar' }); + + expect(() => VpcLatticeV2Envelope.parse(testEvent, TestSchema)).toThrow(); + }); + }); + + describe('safeParse', () => { + it('should parse VPC Lattice event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.vpcLatticeV2Event as VpcLatticeEventV2; + + testEvent.body = JSON.stringify(mock); + + const resp = VpcLatticeV2Envelope.safeParse(testEvent, TestSchema); + + expect(resp).toEqual({ success: true, data: mock }); + }); + + it('should parse VPC Lattice event with trailing slash', () => { + const mock = generateMock(TestSchema); + const testEvent = + TestEvents.vpcLatticeEventV2PathTrailingSlash as VpcLatticeEventV2; + + testEvent.body = JSON.stringify(mock); + + const resp = VpcLatticeV2Envelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ success: true, data: mock }); + }); + + it('should return error if event is not a VPC Lattice event', () => { + const resp = VpcLatticeV2Envelope.safeParse({ foo: 'bar' }, TestSchema); + + expect(resp).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); + + it('should return error if body does not match schema', () => { + const testEvent = TestEvents.vpcLatticeV2Event as VpcLatticeEventV2; + + testEvent.body = JSON.stringify({ foo: 'bar' }); + + const resp = VpcLatticeV2Envelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/parser.decorator.test.ts b/packages/parser/tests/unit/parser.decorator.test.ts new file mode 100644 index 0000000000..dcc4dc6756 --- /dev/null +++ b/packages/parser/tests/unit/parser.decorator.test.ts @@ -0,0 +1,202 @@ +/** + * Test decorator parser + * + * @group unit/parser + */ + +import type { LambdaInterface } from '@aws-lambda-powertools/commons/lib/esm/types'; +import { Context } from 'aws-lambda'; +import { parser } from '../../src/index.js'; +import { TestSchema, TestEvents } from './schema/utils'; +import { generateMock } from '@anatine/zod-mock'; +import { EventBridgeSchema } from '../../src/schemas/index.js'; +import { z } from 'zod'; +import { ParsedResult, EventBridgeEvent } from '../../src/types'; +import { EventBridgeEnvelope } from '../../src/envelopes/index.js'; +import { ParseError } from '../../src/errors.js'; + +describe('Parser Decorator', () => { + const customEventBridgeSchema = EventBridgeSchema.extend({ + detail: TestSchema, + }); + + type TestEvent = z.infer; + + class TestClass implements LambdaInterface { + @parser({ schema: TestSchema }) + public async handler( + event: TestEvent, + _context: Context + ): Promise { + return event; + } + + @parser({ schema: customEventBridgeSchema }) + public async handlerWithCustomSchema( + event: unknown, + _context: Context + ): Promise { + return event; + } + + @parser({ schema: TestSchema, envelope: EventBridgeEnvelope }) + public async handlerWithParserCallsAnotherMethod( + event: TestEvent, + _context: Context + ): Promise { + return this.anotherMethod(event); + } + + @parser({ schema: TestSchema, envelope: EventBridgeEnvelope }) + public async handlerWithSchemaAndEnvelope( + event: TestEvent, + _context: Context + ): Promise { + return event; + } + + @parser({ + schema: TestSchema, + safeParse: true, + }) + public async handlerWithSchemaAndSafeParse( + event: ParsedResult, + _context: Context + ): Promise { + return event; + } + + @parser({ + schema: TestSchema, + envelope: EventBridgeEnvelope, + safeParse: true, + }) + public async harndlerWithEnvelopeAndSafeParse( + event: ParsedResult, + _context: Context + ): Promise { + return event; + } + + private async anotherMethod(event: TestEvent): Promise { + return event; + } + } + + const lambda = new TestClass(); + + it('should parse custom schema event', async () => { + const testEvent = generateMock(TestSchema); + + const resp = await lambda.handler(testEvent, {} as Context); + + expect(resp).toEqual(testEvent); + }); + + it('should parse custom schema with envelope event', async () => { + const customPayload = generateMock(TestSchema); + const testEvent = TestEvents.eventBridgeEvent as EventBridgeEvent; + testEvent.detail = customPayload; + + const resp = await lambda.handlerWithSchemaAndEnvelope( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + testEvent, + {} as Context + ); + + expect(resp).toEqual(customPayload); + }); + + it('should parse extended envelope event', async () => { + const customPayload = generateMock(TestSchema); + + const testEvent = generateMock(customEventBridgeSchema); + testEvent.detail = customPayload; + + const resp: z.infer = + (await lambda.handlerWithCustomSchema( + testEvent, + {} as Context + )) as z.infer; + + expect(customEventBridgeSchema.parse(resp)).toEqual(testEvent); + expect(resp.detail).toEqual(customPayload); + }); + + it('should parse and call private async method', async () => { + const customPayload = generateMock(TestSchema); + const testEvent = TestEvents.eventBridgeEvent as EventBridgeEvent; + testEvent.detail = customPayload; + + const resp = await lambda.handlerWithParserCallsAnotherMethod( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + testEvent, + {} as Context + ); + + expect(resp).toEqual(customPayload); + }); + + it('should parse event with schema and safeParse', async () => { + const testEvent = generateMock(TestSchema); + + const resp = await lambda.handlerWithSchemaAndSafeParse( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + testEvent, + {} as Context + ); + + expect(resp).toEqual({ + success: true, + data: testEvent, + }); + }); + + it('should parse event with schema and safeParse and return error', async () => { + expect( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await lambda.handlerWithSchemaAndSafeParse({ foo: 'bar' }, {} as Context) + ).toEqual({ + error: expect.any(ParseError), + success: false, + originalEvent: { foo: 'bar' }, + }); + }); + + it('should parse event with envelope and safeParse', async () => { + const testEvent = generateMock(TestSchema); + const event = TestEvents.eventBridgeEvent as EventBridgeEvent; + event.detail = testEvent; + + const resp = await lambda.harndlerWithEnvelopeAndSafeParse( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + event, + {} as Context + ); + + expect(resp).toEqual({ + success: true, + data: testEvent, + }); + }); + + it('should parse event with envelope and safeParse and return error', async () => { + expect( + await lambda.harndlerWithEnvelopeAndSafeParse( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + { foo: 'bar' }, + {} as Context + ) + ).toEqual({ + error: expect.any(ParseError), + success: false, + originalEvent: { foo: 'bar' }, + }); + }); +}); diff --git a/packages/parser/tests/unit/parser.middy.test.ts b/packages/parser/tests/unit/parser.middy.test.ts new file mode 100644 index 0000000000..d8dd4ae1b1 --- /dev/null +++ b/packages/parser/tests/unit/parser.middy.test.ts @@ -0,0 +1,192 @@ +/** + * Test middleware parser + * + * @group unit/parser + */ + +import middy from '@middy/core'; +import { Context } from 'aws-lambda'; +import { parser } from '../../src/middleware/parser.js'; +import { generateMock } from '@anatine/zod-mock'; +import { SqsSchema } from '../../src/schemas/index.js'; +import { z, type ZodSchema } from 'zod'; +import { SqsEnvelope, EventBridgeEnvelope } from '../../src/envelopes/index.js'; +import { TestSchema, TestEvents } from './schema/utils'; +import { EventBridgeEvent } from '../../src/types/index.js'; + +describe('Middleware: parser', () => { + type schema = z.infer; + const handler = async ( + event: unknown, + _context: Context + ): Promise => { + return event; + }; + + describe(' when envelope is provided ', () => { + const middyfiedHandlerSchemaEnvelope = middy(handler).use( + parser({ schema: TestSchema, envelope: SqsEnvelope }) + ); + + it('should parse request body with schema and envelope', async () => { + const bodyMock = generateMock(TestSchema); + parser({ schema: TestSchema, envelope: SqsEnvelope }); + + const event = generateMock(SqsSchema, { + stringMap: { + body: () => JSON.stringify(bodyMock), + }, + }); + + const result = (await middyfiedHandlerSchemaEnvelope( + event, + {} as Context + )) as schema[]; + result.forEach((item) => { + expect(item).toEqual(bodyMock); + }); + }); + + it('should throw when envelope does not match', async () => { + await expect(async () => { + await middyfiedHandlerSchemaEnvelope( + { name: 'John', age: 18 }, + {} as Context + ); + }).rejects.toThrow(); + }); + + it('should throw when schema does not match', async () => { + const event = generateMock(SqsSchema, { + stringMap: { + body: () => '42', + }, + }); + + await expect( + middyfiedHandlerSchemaEnvelope(event, {} as Context) + ).rejects.toThrow(); + }); + it('should throw when provided schema is invalid', async () => { + const middyfiedHandler = middy(handler).use( + parser({ schema: {} as ZodSchema, envelope: SqsEnvelope }) + ); + + await expect(middyfiedHandler(42, {} as Context)).rejects.toThrow(); + }); + it('should throw when envelope is correct but schema is invalid', async () => { + const event = generateMock(SqsSchema, { + stringMap: { + body: () => JSON.stringify({ name: 'John', foo: 'bar' }), + }, + }); + + const middyfiedHandler = middy(handler).use( + parser({ schema: {} as ZodSchema, envelope: SqsEnvelope }) + ); + + await expect(middyfiedHandler(event, {} as Context)).rejects.toThrow(); + }); + }); + + describe(' when envelope is not provided', () => { + it('should parse the event with built-in schema', async () => { + const event = generateMock(SqsSchema); + + const middyfiedHandler = middy(handler).use( + parser({ schema: SqsSchema }) + ); + + expect(await middyfiedHandler(event, {} as Context)).toEqual(event); + }); + + it('should parse custom event', async () => { + const event = { name: 'John', age: 18 }; + const middyfiedHandler = middy(handler).use( + parser({ schema: TestSchema }) + ); + + expect(await middyfiedHandler(event, {} as Context)).toEqual(event); + }); + + it('should throw when the schema does not match', async () => { + const middyfiedHandler = middy(handler).use( + parser({ schema: TestSchema }) + ); + + await expect(middyfiedHandler(42, {} as Context)).rejects.toThrow(); + }); + + it('should throw when provided schema is invalid', async () => { + const middyfiedHandler = middy(handler).use( + parser({ schema: {} as ZodSchema }) + ); + + await expect( + middyfiedHandler({ foo: 'bar' }, {} as Context) + ).rejects.toThrow(); + }); + + it('should return the event when safeParse is true', async () => { + const event = { name: 'John', age: 18 }; + const middyfiedHandler = middy(handler).use( + parser({ schema: TestSchema, safeParse: true }) + ); + + expect(await middyfiedHandler(event, {} as Context)).toEqual({ + success: true, + data: event, + }); + }); + + it('should return error when safeParse is true and schema does not match', async () => { + const middyfiedHandler = middy(handler).use( + parser({ schema: TestSchema, safeParse: true }) + ); + + expect(await middyfiedHandler(42, {} as Context)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: 42, + }); + }); + + it('should return event when envelope and safeParse are true', async () => { + const detail = generateMock(TestSchema); + const event = TestEvents.eventBridgeEvent as EventBridgeEvent; + + event.detail = detail; + + const middyfiedHandler = middy(handler).use( + parser({ + schema: TestSchema, + envelope: EventBridgeEnvelope, + safeParse: true, + }) + ); + + expect(await middyfiedHandler(event, {} as Context)).toEqual({ + success: true, + data: detail, + }); + }); + + it('should return error when envelope and safeParse are true and schema does not match', async () => { + const event = TestEvents.eventBridgeEvent as EventBridgeEvent; + + const middyfiedHandler = middy(handler).use( + parser({ + schema: TestSchema, + envelope: EventBridgeEnvelope, + safeParse: true, + }) + ); + + expect(await middyfiedHandler(event, {} as Context)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: event, + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/schema/alb.test.ts b/packages/parser/tests/unit/schema/alb.test.ts new file mode 100644 index 0000000000..6984f56f6d --- /dev/null +++ b/packages/parser/tests/unit/schema/alb.test.ts @@ -0,0 +1,27 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ +import { AlbSchema, AlbMultiValueHeadersSchema } from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('ALB ', () => { + it('should parse alb event', () => { + const albEvent = TestEvents.albEvent; + expect(AlbSchema.parse(albEvent)).toEqual(albEvent); + }); + it('should parse alb event path trailing slash', () => { + const albEventPathTrailingSlash = TestEvents.albEventPathTrailingSlash; + expect(AlbSchema.parse(albEventPathTrailingSlash)).toEqual( + albEventPathTrailingSlash + ); + }); + it('should parse alb event with multi value headers event', () => { + const albMultiValueHeadersEvent = TestEvents.albMultiValueHeadersEvent; + + expect(AlbMultiValueHeadersSchema.parse(albMultiValueHeadersEvent)).toEqual( + albMultiValueHeadersEvent + ); + }); +}); diff --git a/packages/parser/tests/unit/schema/apigw.test.ts b/packages/parser/tests/unit/schema/apigw.test.ts new file mode 100644 index 0000000000..8b03c9ffbe --- /dev/null +++ b/packages/parser/tests/unit/schema/apigw.test.ts @@ -0,0 +1,100 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { APIGatewayProxyEventSchema } from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('APIGateway ', () => { + it('should parse api gateway event', () => { + const apiGatewayProxyEvent = TestEvents.apiGatewayProxyEvent; + + expect(APIGatewayProxyEventSchema.parse(apiGatewayProxyEvent)).toEqual( + apiGatewayProxyEvent + ); + }); + it('should parse api gateway authorizer request event', () => { + const apiGatewayAuthorizerRequestEvent = + TestEvents.apiGatewayAuthorizerRequestEvent; + + expect( + APIGatewayProxyEventSchema.parse(apiGatewayAuthorizerRequestEvent) + ).toEqual(apiGatewayAuthorizerRequestEvent); + }); + it('should parse schema middleware invalid event', () => { + const apiGatewaySchemaMiddlewareInvalidEvent = + TestEvents.apiGatewaySchemaMiddlewareInvalidEvent; + + expect( + APIGatewayProxyEventSchema.parse(apiGatewaySchemaMiddlewareInvalidEvent) + ).toEqual(apiGatewaySchemaMiddlewareInvalidEvent); + }); + it('should parse schema middleware valid event', () => { + const apiGatewaySchemaMiddlewareValidEvent = + TestEvents.apiGatewaySchemaMiddlewareValidEvent; + + expect( + APIGatewayProxyEventSchema.parse(apiGatewaySchemaMiddlewareValidEvent) + ).toEqual(apiGatewaySchemaMiddlewareValidEvent); + }); + it('should parse proxy event with no version auth', () => { + const apiGatewayProxyEvent_noVersionAuth = + TestEvents.apiGatewayProxyEvent_noVersionAuth; + + expect( + APIGatewayProxyEventSchema.parse(apiGatewayProxyEvent_noVersionAuth) + ).toEqual(apiGatewayProxyEvent_noVersionAuth); + }); + it('should parse proxy event with another path', () => { + const apiGatewayProxyEventAnotherPath = + TestEvents.apiGatewayProxyEventAnotherPath; + + expect( + APIGatewayProxyEventSchema.parse(apiGatewayProxyEventAnotherPath) + ).toEqual(apiGatewayProxyEventAnotherPath); + }); + it('should parse proxy event with path trailing slash', () => { + const apiGatewayProxyEventPathTrailingSlash = + TestEvents.apiGatewayProxyEventPathTrailingSlash; + expect( + APIGatewayProxyEventSchema.parse(apiGatewayProxyEventPathTrailingSlash) + ).toEqual(apiGatewayProxyEventPathTrailingSlash); + }); + it('should parse other proxy event', () => { + const apiGatewayProxyOtherEvent = TestEvents.apiGatewayProxyOtherEvent; + expect(APIGatewayProxyEventSchema.parse(apiGatewayProxyOtherEvent)).toEqual( + apiGatewayProxyOtherEvent + ); + }); + it('should throw error when event is not a valid proxy event', () => { + const event = { + resource: '/', + path: '/', + httpMethod: 'GET', + headers: {}, + multiValueHeaders: {}, + isBase64Encoded: false, + body: 'Foo!', + requestContext: { + accountId: '1234', + apiId: 'myApi', + httpMethod: 'GET', + identity: { + sourceIp: '127.0.0.1', + }, + path: '/', + protocol: 'Https', + requestId: '1234', + requestTime: '2018-09-07T16:20:46Z', + requestTimeEpoch: 1536992496000, + resourcePath: '/', + stage: 'test', + eventType: 'DISCONNECT', + messageId: 'messageId', + }, + }; + expect(() => APIGatewayProxyEventSchema.parse(event)).toThrow(); + }); +}); diff --git a/packages/parser/tests/unit/schema/apigwv2.test.ts b/packages/parser/tests/unit/schema/apigwv2.test.ts new file mode 100644 index 0000000000..1ebb547c7f --- /dev/null +++ b/packages/parser/tests/unit/schema/apigwv2.test.ts @@ -0,0 +1,67 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { APIGatewayProxyEventV2Schema } from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('API GW v2 ', () => { + it('should parse api gateway v2 event', () => { + const apiGatewayProxyV2Event = TestEvents.apiGatewayProxyV2Event; + + expect(APIGatewayProxyEventV2Schema.parse(apiGatewayProxyV2Event)).toEqual( + apiGatewayProxyV2Event + ); + }); + it('should parse api gateway v2 event with GET method', () => { + const apiGatewayProxyV2Event_GET = TestEvents.apiGatewayProxyV2Event_GET; + expect( + APIGatewayProxyEventV2Schema.parse(apiGatewayProxyV2Event_GET) + ).toEqual(apiGatewayProxyV2Event_GET); + }); + it('should parse api gateway v2 event with path trailing slash', () => { + const apiGatewayProxyV2EventPathTrailingSlash = + TestEvents.apiGatewayProxyV2EventPathTrailingSlash; + + expect( + APIGatewayProxyEventV2Schema.parse( + apiGatewayProxyV2EventPathTrailingSlash + ) + ).toEqual(apiGatewayProxyV2EventPathTrailingSlash); + }); + it('should parse api gateway v2 event with iam', () => { + const apiGatewayProxyV2IamEvent = TestEvents.apiGatewayProxyV2IamEvent; + + expect( + APIGatewayProxyEventV2Schema.parse(apiGatewayProxyV2IamEvent) + ).toEqual(apiGatewayProxyV2IamEvent); + }); + it('should parse api gateway v2 event with lambda authorizer', () => { + const apiGatewayProxyV2LambdaAuthorizerEvent = + TestEvents.apiGatewayProxyV2LambdaAuthorizerEvent; + + expect( + APIGatewayProxyEventV2Schema.parse(apiGatewayProxyV2LambdaAuthorizerEvent) + ).toEqual(apiGatewayProxyV2LambdaAuthorizerEvent); + }); + it('should parse api gateway v2 event with other get event', () => { + const apiGatewayProxyV2OtherGetEvent = + TestEvents.apiGatewayProxyV2OtherGetEvent; + + expect( + APIGatewayProxyEventV2Schema.parse(apiGatewayProxyV2OtherGetEvent) + ).toEqual(apiGatewayProxyV2OtherGetEvent); + }); + it('should parse api gateway v2 event with schema middleware', () => { + const apiGatewayProxyV2SchemaMiddlewareValidEvent = + TestEvents.apiGatewayProxyV2SchemaMiddlewareValidEvent; + + expect( + APIGatewayProxyEventV2Schema.parse( + apiGatewayProxyV2SchemaMiddlewareValidEvent + ) + ).toEqual(apiGatewayProxyV2SchemaMiddlewareValidEvent); + }); +}); diff --git a/packages/parser/tests/unit/schema/cloudformation-custom-resource.test.ts b/packages/parser/tests/unit/schema/cloudformation-custom-resource.test.ts new file mode 100644 index 0000000000..7b4df56201 --- /dev/null +++ b/packages/parser/tests/unit/schema/cloudformation-custom-resource.test.ts @@ -0,0 +1,45 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { + CloudFormationCustomResourceCreateSchema, + CloudFormationCustomResourceUpdateSchema, + CloudFormationCustomResourceDeleteSchema, +} from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('CloudFormationCustomResource ', () => { + it('should parse create event', () => { + const cloudFormationCustomResourceCreateEvent = + TestEvents.cloudFormationCustomResourceCreateEvent; + + expect( + CloudFormationCustomResourceCreateSchema.parse( + cloudFormationCustomResourceCreateEvent + ) + ).toEqual(cloudFormationCustomResourceCreateEvent); + }); + it('should parse update event', () => { + const cloudFormationCustomResourceUpdateEvent = + TestEvents.cloudFormationCustomResourceUpdateEvent; + + expect( + CloudFormationCustomResourceUpdateSchema.parse( + cloudFormationCustomResourceUpdateEvent + ) + ).toEqual(cloudFormationCustomResourceUpdateEvent); + }); + it('should parse delete event', () => { + const cloudFormationCustomResourceDeleteEvent = + TestEvents.cloudFormationCustomResourceDeleteEvent; + + expect( + CloudFormationCustomResourceDeleteSchema.parse( + cloudFormationCustomResourceDeleteEvent + ) + ).toEqual(cloudFormationCustomResourceDeleteEvent); + }); +}); diff --git a/packages/parser/tests/unit/schema/cloudwatch.test.ts b/packages/parser/tests/unit/schema/cloudwatch.test.ts new file mode 100644 index 0000000000..e126bdb7fb --- /dev/null +++ b/packages/parser/tests/unit/schema/cloudwatch.test.ts @@ -0,0 +1,30 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { CloudWatchLogsSchema } from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('CloudWatchLogs ', () => { + it('should parse cloudwatch logs event', () => { + const cloudWatchLogEvent = TestEvents.cloudWatchLogEvent; + const parsed = CloudWatchLogsSchema.parse(cloudWatchLogEvent); + expect(parsed.awslogs.data).toBeDefined(); + expect(parsed.awslogs.data?.logEvents[0]).toEqual({ + id: 'eventId1', + timestamp: 1440442987000, + message: '[ERROR] First test message', + }); + }); + it('should throw error if cloudwatch logs event is invalid', () => { + expect(() => + CloudWatchLogsSchema.parse({ + awslogs: { + data: 'invalid', + }, + }) + ).toThrowError(); + }); +}); diff --git a/packages/parser/tests/unit/schema/dynamodb.test.ts b/packages/parser/tests/unit/schema/dynamodb.test.ts new file mode 100644 index 0000000000..f0d90fbc5b --- /dev/null +++ b/packages/parser/tests/unit/schema/dynamodb.test.ts @@ -0,0 +1,17 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { DynamoDBStreamSchema } from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('DynamoDB ', () => { + const dynamoStreamEvent = TestEvents.dynamoStreamEvent; + it('should parse a stream of records', () => { + expect(DynamoDBStreamSchema.parse(dynamoStreamEvent)).toEqual( + dynamoStreamEvent + ); + }); +}); diff --git a/packages/parser/tests/unit/schema/eventbridge.test.ts b/packages/parser/tests/unit/schema/eventbridge.test.ts new file mode 100644 index 0000000000..4423318874 --- /dev/null +++ b/packages/parser/tests/unit/schema/eventbridge.test.ts @@ -0,0 +1,16 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { EventBridgeSchema } from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('EventBridge ', () => { + it('should parse eventbridge event', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent; + + expect(EventBridgeSchema.parse(eventBridgeEvent)).toEqual(eventBridgeEvent); + }); +}); diff --git a/packages/parser/tests/unit/schema/kafka.test.ts b/packages/parser/tests/unit/schema/kafka.test.ts new file mode 100644 index 0000000000..c6aab47348 --- /dev/null +++ b/packages/parser/tests/unit/schema/kafka.test.ts @@ -0,0 +1,63 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { + KafkaMskEventSchema, + KafkaSelfManagedEventSchema, +} from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('Kafka ', () => { + const expectedTestEvent = { + key: 'recordKey', + value: JSON.stringify({ key: 'value' }), + partition: 0, + topic: 'mytopic', + offset: 15, + timestamp: 1545084650987, + timestampType: 'CREATE_TIME', + headers: [ + { + headerKey: 'headerValue', + }, + ], + }; + it('should parse kafka MSK event', () => { + const kafkaEventMsk = TestEvents.kafkaEventMsk; + + expect( + KafkaMskEventSchema.parse(kafkaEventMsk).records['mytopic-0'][0] + ).toEqual(expectedTestEvent); + }); + it('should parse kafka self managed event', () => { + const kafkaEventSelfManaged = TestEvents.kafkaEventSelfManaged; + + expect( + KafkaSelfManagedEventSchema.parse(kafkaEventSelfManaged).records[ + 'mytopic-0' + ][0] + ).toEqual(expectedTestEvent); + }); + it('should transform bootstrapServers to array', () => { + const kafkaEventSelfManaged = TestEvents.kafkaEventSelfManaged; + + expect( + KafkaSelfManagedEventSchema.parse(kafkaEventSelfManaged).bootstrapServers + ).toEqual([ + 'b-2.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092', + 'b-1.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092', + ]); + }); + it('should return undefined if bootstrapServers is not present', () => { + const kafkaEventSelfManaged = TestEvents.kafkaEventSelfManaged as { + bootstrapServers: string; + }; + kafkaEventSelfManaged.bootstrapServers = ''; + const parsed = KafkaSelfManagedEventSchema.parse(kafkaEventSelfManaged); + + expect(parsed.bootstrapServers).toBeUndefined(); + }); +}); diff --git a/packages/parser/tests/unit/schema/kinesis.test.ts b/packages/parser/tests/unit/schema/kinesis.test.ts new file mode 100644 index 0000000000..7d66b99469 --- /dev/null +++ b/packages/parser/tests/unit/schema/kinesis.test.ts @@ -0,0 +1,72 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { + KinesisFirehoseSchema, + KinesisFirehoseSqsSchema, + KinesisDataStreamSchema, +} from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('Kinesis ', () => { + it('should parse kinesis event', () => { + const kinesisStreamEvent = TestEvents.kinesisStreamEvent; + const parsed = KinesisDataStreamSchema.parse(kinesisStreamEvent); + + expect(parsed.Records[0].kinesis.data).toEqual('Hello, this is a test.'); + }); + it('should parse single kinesis record', () => { + const kinesisStreamEventOneRecord = TestEvents.kinesisStreamEventOneRecord; + const parsed = KinesisDataStreamSchema.parse(kinesisStreamEventOneRecord); + + expect(parsed.Records[0].kinesis.data).toEqual({ + message: 'test message', + username: 'test', + }); + }); + it('should parse Firehose event', () => { + const kinesisFirehoseKinesisEvent = TestEvents.kinesisFirehoseKinesisEvent; + const parsed = KinesisFirehoseSchema.parse(kinesisFirehoseKinesisEvent); + expect(parsed.records[0].data).toEqual('Hello World'); + }); + it('should parse Kinesis Firehose PutEvents event', () => { + const kinesisFirehosePutEvent = TestEvents.kinesisFirehosePutEvent; + const parsed = KinesisFirehoseSchema.parse(kinesisFirehosePutEvent); + expect(JSON.parse(parsed.records[1].data)).toEqual({ + Hello: 'World', + }); + }); + it('should parse Firehose event with SQS event', () => { + const kinesisFirehoseSQSEvent = TestEvents.kinesisFirehoseSQSEvent; + const parsed = KinesisFirehoseSqsSchema.parse(kinesisFirehoseSQSEvent); + expect(parsed.records[0].data).toMatchObject({ + messageId: '5ab807d4-5644-4c55-97a3-47396635ac74', + body: 'Test message.', + }); + }); + it('should parse Kinesis event with CloudWatch event', () => { + const kinesisStreamCloudWatchLogsEvent = + TestEvents.kinesisStreamCloudWatchLogsEvent; + const parsed = KinesisDataStreamSchema.parse( + kinesisStreamCloudWatchLogsEvent + ); + + expect(parsed.Records[0].kinesis.data).toMatchObject({ + messageType: 'DATA_MESSAGE', + owner: '231436140809', + logGroup: '/aws/lambda/pt-1488-DummyLogDataFunction-gnWXPvL6jJyG', + logStream: '2022/11/10/[$LATEST]26b6a45d574f442ea28438923cbf7bf7', + }); + }); + it('should return original value if cannot parse KinesisFirehoseSqsRecord', () => { + const kinesisFirehoseSQSEvent = TestEvents.kinesisFirehoseSQSEvent as { + records: { data: string }[]; + }; + kinesisFirehoseSQSEvent.records[0].data = 'not a valid json'; + const parsed = KinesisFirehoseSqsSchema.parse(kinesisFirehoseSQSEvent); + expect(parsed.records[0].data).toEqual('not a valid json'); + }); +}); diff --git a/packages/parser/tests/unit/schema/lambda.test.ts b/packages/parser/tests/unit/schema/lambda.test.ts new file mode 100644 index 0000000000..cfef867657 --- /dev/null +++ b/packages/parser/tests/unit/schema/lambda.test.ts @@ -0,0 +1,18 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { LambdaFunctionUrlSchema } from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('Lambda ', () => { + it('should parse lambda event', () => { + const lambdaFunctionUrlEvent = TestEvents.apiGatewayProxyV2Event; + + expect(LambdaFunctionUrlSchema.parse(lambdaFunctionUrlEvent)).toEqual( + lambdaFunctionUrlEvent + ); + }); +}); diff --git a/packages/parser/tests/unit/schema/s3.test.ts b/packages/parser/tests/unit/schema/s3.test.ts new file mode 100644 index 0000000000..72e3ba74dc --- /dev/null +++ b/packages/parser/tests/unit/schema/s3.test.ts @@ -0,0 +1,105 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { + S3EventNotificationEventBridgeSchema, + S3SqsEventNotificationSchema, + S3Schema, + S3ObjectLambdaEventSchema, +} from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('S3 ', () => { + it('should parse s3 event', () => { + const s3Event = TestEvents.s3Event; + + expect(S3Schema.parse(s3Event)).toEqual(s3Event); + }); + + it('should parse s3 event bridge notification event created', () => { + const s3EventBridgeNotificationObjectCreatedEvent = + TestEvents.s3EventBridgeNotificationObjectCreatedEvent; + + expect( + S3EventNotificationEventBridgeSchema.parse( + s3EventBridgeNotificationObjectCreatedEvent + ) + ).toEqual(s3EventBridgeNotificationObjectCreatedEvent); + }); + + it('should parse s3 event bridge notification event detelted', () => { + const s3EventBridgeNotificationObjectDeletedEvent = + TestEvents.s3EventBridgeNotificationObjectDeletedEvent; + + expect( + S3EventNotificationEventBridgeSchema.parse( + s3EventBridgeNotificationObjectDeletedEvent + ) + ).toEqual(s3EventBridgeNotificationObjectDeletedEvent); + }); + it('should parse s3 event bridge notification event expired', () => { + const s3EventBridgeNotificationObjectExpiredEvent = + TestEvents.s3EventBridgeNotificationObjectExpiredEvent; + + expect( + S3EventNotificationEventBridgeSchema.parse( + s3EventBridgeNotificationObjectExpiredEvent + ) + ).toEqual(s3EventBridgeNotificationObjectExpiredEvent); + }); + + it('should parse s3 sqs notification event', () => { + const s3SqsEvent = TestEvents.s3SqsEvent; + expect(S3SqsEventNotificationSchema.parse(s3SqsEvent)).toEqual(s3SqsEvent); + }); + + it('should parse s3 event with decoded key', () => { + const s3EventDecodedKey = TestEvents.s3EventDecodedKey; + expect(S3Schema.parse(s3EventDecodedKey)).toEqual(s3EventDecodedKey); + }); + + it('should parse s3 event delete object', () => { + const s3EventDeleteObject = TestEvents.s3EventDeleteObject; + expect(S3Schema.parse(s3EventDeleteObject)).toEqual(s3EventDeleteObject); + }); + + it('should parse s3 event glacier', () => { + const s3EventGlacier = TestEvents.s3EventGlacier; + expect(S3Schema.parse(s3EventGlacier)).toEqual(s3EventGlacier); + }); + + it('should parse s3 object event iam user', () => { + const s3ObjectEventIAMUser = TestEvents.s3ObjectEventIAMUser; + expect(S3ObjectLambdaEventSchema.parse(s3ObjectEventIAMUser)).toEqual( + s3ObjectEventIAMUser + ); + }); + + it('should parse s3 object event temp credentials', () => { + // ignore any because we don't want typed json + const s3ObjectEventTempCredentials = + TestEvents.s3ObjectEventTempCredentials as any; // eslint-disable-line @typescript-eslint/no-explicit-any + const parsed = S3ObjectLambdaEventSchema.parse( + s3ObjectEventTempCredentials + ); + + expect(parsed.userRequest).toEqual( + s3ObjectEventTempCredentials.userRequest + ); + expect(parsed.getObjectContext).toEqual( + s3ObjectEventTempCredentials.getObjectContext + ); + expect(parsed.configuration).toEqual( + s3ObjectEventTempCredentials.configuration + ); + expect(parsed.userRequest).toEqual( + s3ObjectEventTempCredentials.userRequest + ); + expect( + parsed.userIdentity?.sessionContext?.attributes.mfaAuthenticated + ).toEqual(false); + }); +}); diff --git a/packages/parser/tests/unit/schema/ses.test.ts b/packages/parser/tests/unit/schema/ses.test.ts new file mode 100644 index 0000000000..010fa4872c --- /dev/null +++ b/packages/parser/tests/unit/schema/ses.test.ts @@ -0,0 +1,15 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { SesSchema } from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('Schema:', () => { + it('SES should parse ses event', () => { + const sesEvent = TestEvents.sesEvent; + expect(SesSchema.parse(sesEvent)).toEqual(sesEvent); + }); +}); diff --git a/packages/parser/tests/unit/schema/sns.test.ts b/packages/parser/tests/unit/schema/sns.test.ts new file mode 100644 index 0000000000..26a212b7ad --- /dev/null +++ b/packages/parser/tests/unit/schema/sns.test.ts @@ -0,0 +1,15 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { SnsSchema } from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('Schema:', () => { + it('SNS should parse sns event', () => { + const snsEvent = TestEvents.snsEvent; + expect(SnsSchema.parse(snsEvent)).toEqual(snsEvent); + }); +}); diff --git a/packages/parser/tests/unit/schema/sqs.test.ts b/packages/parser/tests/unit/schema/sqs.test.ts new file mode 100644 index 0000000000..d92f0d73e5 --- /dev/null +++ b/packages/parser/tests/unit/schema/sqs.test.ts @@ -0,0 +1,15 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { SqsSchema } from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('SQS ', () => { + it('should parse sqs event', () => { + const sqsEvent = TestEvents.sqsEvent; + expect(SqsSchema.parse(sqsEvent)).toEqual(sqsEvent); + }); +}); diff --git a/packages/parser/tests/unit/schema/utils.ts b/packages/parser/tests/unit/schema/utils.ts new file mode 100644 index 0000000000..3a17df8570 --- /dev/null +++ b/packages/parser/tests/unit/schema/utils.ts @@ -0,0 +1,116 @@ +import { readFileSync } from 'node:fs'; +import { z } from 'zod'; + +export const TestSchema = z.object({ + name: z.string(), + age: z.number().min(18).max(99), +}); + +const filenames = [ + 'activeMQEvent', + 'albEvent', + 'albEventPathTrailingSlash', + 'albMultiValueHeadersEvent', + 'apiGatewayAuthorizerRequestEvent', + 'apiGatewayAuthorizerTokenEvent', + 'apiGatewayAuthorizerV2Event', + 'apiGatewayProxyEvent', + 'apiGatewayProxyEventAnotherPath', + 'apiGatewayProxyEventPathTrailingSlash', + 'apiGatewayProxyEventPrincipalId', + 'apiGatewayProxyEvent_noVersionAuth', + 'apiGatewayProxyOtherEvent', + 'apiGatewayProxyV2Event', + 'apiGatewayProxyV2EventPathTrailingSlash', + 'apiGatewayProxyV2Event_GET', + 'apiGatewayProxyV2IamEvent', + 'apiGatewayProxyV2LambdaAuthorizerEvent', + 'apiGatewayProxyV2OtherGetEvent', + 'apiGatewayProxyV2SchemaMiddlewareInvalidEvent', + 'apiGatewayProxyV2SchemaMiddlewareValidEvent', + 'apiGatewaySchemaMiddlewareInvalidEvent', + 'apiGatewaySchemaMiddlewareValidEvent', + 'appSyncAuthorizerEvent', + 'appSyncAuthorizerResponse', + 'appSyncDirectResolver', + 'appSyncResolverEvent', + 'awsConfigRuleConfigurationChanged', + 'awsConfigRuleOversizedConfiguration', + 'awsConfigRuleScheduled', + 'bedrockAgentEvent', + 'bedrockAgentPostEvent', + 'cloudFormationCustomResourceCreateEvent', + 'cloudFormationCustomResourceDeleteEvent', + 'cloudFormationCustomResourceUpdateEvent', + 'cloudWatchDashboardEvent', + 'cloudWatchLogEvent', + 'codePipelineEvent', + 'codePipelineEventData', + 'codePipelineEventEmptyUserParameters', + 'codePipelineEventWithEncryptionKey', + 'cognitoCreateAuthChallengeEvent', + 'cognitoCustomMessageEvent', + 'cognitoDefineAuthChallengeEvent', + 'cognitoPostAuthenticationEvent', + 'cognitoPostConfirmationEvent', + 'cognitoPreAuthenticationEvent', + 'cognitoPreSignUpEvent', + 'cognitoPreTokenGenerationEvent', + 'cognitoUserMigrationEvent', + 'cognitoVerifyAuthChallengeResponseEvent', + 'connectContactFlowEventAll', + 'connectContactFlowEventMin', + 'dynamoStreamEvent', + 'eventBridgeEvent', + 'kafkaEventMsk', + 'kafkaEventSelfManaged', + 'kinesisFirehoseKinesisEvent', + 'kinesisFirehosePutEvent', + 'kinesisFirehoseSQSEvent', + 'kinesisStreamCloudWatchLogsEvent', + 'kinesisStreamEvent', + 'kinesisStreamEventOneRecord', + 'lambdaFunctionUrlEvent', + 'lambdaFunctionUrlEventPathTrailingSlash', + 'lambdaFunctionUrlIAMEvent', + 'rabbitMQEvent', + 's3Event', + 's3EventBridgeNotificationObjectCreatedEvent', + 's3EventBridgeNotificationObjectDeletedEvent', + 's3EventBridgeNotificationObjectExpiredEvent', + 's3EventBridgeNotificationObjectRestoreCompletedEvent', + 's3EventDecodedKey', + 's3EventDeleteObject', + 's3EventGlacier', + 's3ObjectEventIAMUser', + 's3ObjectEventTempCredentials', + 's3SqsEvent', + 'secretsManagerEvent', + 'sesEvent', + 'snsEvent', + 'snsSqsEvent', + 'snsSqsFifoEvent', + 'sqsEvent', + 'vpcLatticeEvent', + 'vpcLatticeEventPathTrailingSlash', + 'vpcLatticeEventV2PathTrailingSlash', + 'vpcLatticeV2Event', +] as const; + +type TestEvents = { [K in (typeof filenames)[number]]: unknown }; +const loadFileContent = (filename: string): string => + readFileSync(`./tests/events/${filename}.json`, 'utf-8'); + +const createTestEvents = (fileList: readonly string[]): TestEvents => { + const testEvents: Partial = {}; + + fileList.forEach((filename) => { + Object.defineProperty(testEvents, filename, { + get: () => JSON.parse(loadFileContent(filename)), + }); + }); + + return testEvents as TestEvents; +}; + +export const TestEvents = createTestEvents(filenames); diff --git a/packages/parser/tests/unit/schema/vpc-lattice.test.ts b/packages/parser/tests/unit/schema/vpc-lattice.test.ts new file mode 100644 index 0000000000..643714fb47 --- /dev/null +++ b/packages/parser/tests/unit/schema/vpc-lattice.test.ts @@ -0,0 +1,22 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { VpcLatticeSchema } from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('VPC Lattice ', () => { + it('should parse vpc lattice event', () => { + const vpcLatticeEvent = TestEvents.vpcLatticeEvent; + expect(VpcLatticeSchema.parse(vpcLatticeEvent)).toEqual(vpcLatticeEvent); + }); + it('should parse vpc lattice path trailing slash event', () => { + const vpcLatticeEventPathTrailingSlash = + TestEvents.vpcLatticeEventPathTrailingSlash; + expect(VpcLatticeSchema.parse(vpcLatticeEventPathTrailingSlash)).toEqual( + vpcLatticeEventPathTrailingSlash + ); + }); +}); diff --git a/packages/parser/tests/unit/schema/vpc-latticev2.test.ts b/packages/parser/tests/unit/schema/vpc-latticev2.test.ts new file mode 100644 index 0000000000..5e1b6c7343 --- /dev/null +++ b/packages/parser/tests/unit/schema/vpc-latticev2.test.ts @@ -0,0 +1,23 @@ +/** + * Test built in schema + * + * @group unit/parser/schema/ + */ + +import { VpcLatticeV2Schema } from '../../../src/schemas/'; +import { TestEvents } from './utils.js'; + +describe('VpcLatticeV2 ', () => { + it('should parse VpcLatticeV2 event', () => { + const vpcLatticeV2Event = TestEvents.vpcLatticeV2Event; + const parsed = VpcLatticeV2Schema.parse(vpcLatticeV2Event); + expect(parsed).toEqual(vpcLatticeV2Event); + }); + + it('should parse VpcLatticeV2PathTrailingSlash event', () => { + const vpcLatticeEventV2PathTrailingSlash = + TestEvents.vpcLatticeEventV2PathTrailingSlash; + const parsed = VpcLatticeV2Schema.parse(vpcLatticeEventV2PathTrailingSlash); + expect(parsed).toEqual(vpcLatticeEventV2PathTrailingSlash); + }); +}); diff --git a/packages/parser/tsconfig.esm.json b/packages/parser/tsconfig.esm.json new file mode 100644 index 0000000000..123291b0cf --- /dev/null +++ b/packages/parser/tsconfig.esm.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.esm.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "./lib/esm", + "rootDir": "./src", + "tsBuildInfoFile": ".tsbuildinfo/esm.json" + }, + "include": [ + "./src/**/*" + ] +} \ No newline at end of file diff --git a/packages/parser/tsconfig.json b/packages/parser/tsconfig.json new file mode 100644 index 0000000000..92aecd7c98 --- /dev/null +++ b/packages/parser/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./lib/cjs/", + "rootDir": "./src", + "tsBuildInfoFile": ".tsbuildinfo/cjs.json" + }, + "include": [ + "./src/**/*" + ], +} \ No newline at end of file diff --git a/packages/parser/typedoc.json b/packages/parser/typedoc.json new file mode 100644 index 0000000000..ed0ca6fc47 --- /dev/null +++ b/packages/parser/typedoc.json @@ -0,0 +1,9 @@ +{ + "extends": [ + "../../typedoc.base.json" + ], + "entryPoints": [ + "./src/index.ts" + ], + "readme": "README.md" +} \ No newline at end of file diff --git a/packages/testing/CHANGELOG.md b/packages/testing/CHANGELOG.md index e19e2b234c..ee9d3fff25 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.1.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.0.4...v2.1.0) (2024-04-17) + +**Note:** Version bump only for package @aws-lambda-powertools/testing-utils + + + + + ## 2.0.4 (2024-04-10) **Note:** Version bump only for package @aws-lambda-powertools/testing-utils diff --git a/packages/testing/package.json b/packages/testing/package.json index a10f00cb0b..4d95ed6156 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/testing-utils", - "version": "2.0.4", + "version": "2.1.0", "description": "A package containing utilities to test your serverless workloads", "author": { "name": "Amazon Web Services", @@ -95,9 +95,9 @@ "homepage": "https://github.com/aws-powertools/powertools-lambda-typescript/tree/main/packages/testing#readme", "dependencies": { "@aws-cdk/cli-lib-alpha": "^2.121.1-alpha.0", - "@aws-sdk/client-lambda": "^3.549.0", + "@aws-sdk/client-lambda": "^3.554.0", "@smithy/util-utf8": "^2.3.0", - "aws-cdk-lib": "^2.136.0", + "aws-cdk-lib": "^2.137.0", "esbuild": "^0.20.2" } } diff --git a/packages/testing/src/resources/TestNodejsFunction.ts b/packages/testing/src/resources/TestNodejsFunction.ts index 5be3597caa..5ce138e53e 100644 --- a/packages/testing/src/resources/TestNodejsFunction.ts +++ b/packages/testing/src/resources/TestNodejsFunction.ts @@ -1,6 +1,6 @@ import { CfnOutput, Duration } from 'aws-cdk-lib'; import { Tracing } from 'aws-cdk-lib/aws-lambda'; -import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs'; import { RetentionDays } from 'aws-cdk-lib/aws-logs'; import { randomUUID } from 'node:crypto'; import { TEST_RUNTIMES, TEST_ARCHITECTURES } from '../constants.js'; @@ -23,11 +23,24 @@ class TestNodejsFunction extends NodejsFunction { props: TestNodejsFunctionProps, extraProps: ExtraTestProps ) { + const isESM = extraProps.outputFormat === 'ESM'; + const { bundling, ...restProps } = props; + super(stack.stack, `fn-${randomUUID().substring(0, 5)}`, { timeout: Duration.seconds(30), - memorySize: 256, + memorySize: 512, tracing: Tracing.ACTIVE, - ...props, + bundling: { + ...bundling, + minify: true, + mainFields: isESM ? ['module', 'main'] : ['main', 'module'], + sourceMap: false, + format: isESM ? OutputFormat.ESM : OutputFormat.CJS, + banner: isESM + ? `import { createRequire } from 'module';const require = createRequire(import.meta.url);` + : '', + }, + ...restProps, functionName: concatenateResourceName({ testName: stack.testName, resourceName: extraProps.nameSuffix, diff --git a/packages/testing/src/types.ts b/packages/testing/src/types.ts index ce11c0fc41..1732e2d3b6 100644 --- a/packages/testing/src/types.ts +++ b/packages/testing/src/types.ts @@ -13,6 +13,12 @@ interface ExtraTestProps { * Note that the maximum length of the name is 64 characters, so the suffix might be truncated. */ nameSuffix: string; + /** + * The output format of the bundled code. + * + * @default 'CJS' + */ + outputFormat?: 'CJS' | 'ESM'; } type TestDynamodbTableProps = Omit< @@ -27,8 +33,13 @@ type TestDynamodbTableProps = Omit< type TestNodejsFunctionProps = Omit< NodejsFunctionProps, - 'logRetention' | 'runtime' | 'functionName' ->; + 'logRetention' | 'runtime' | 'functionName' | 'bundling' +> & { + bundling?: Omit< + NodejsFunctionProps['bundling'], + 'minify' | 'mainFields' | 'sourceMap' | 'format' | 'banner' + >; +}; type InvokeTestFunctionOptions = { functionName: string; diff --git a/packages/tracer/CHANGELOG.md b/packages/tracer/CHANGELOG.md index 92342b87b1..d36e2bb6e7 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.1.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v2.0.4...v2.1.0) (2024-04-17) + +**Note:** Version bump only for package @aws-lambda-powertools/tracer + + + + + ## 2.0.4 (2024-04-10) ### Features diff --git a/packages/tracer/package.json b/packages/tracer/package.json index bad0171d8d..a248ea8f4f 100644 --- a/packages/tracer/package.json +++ b/packages/tracer/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/tracer", - "version": "2.0.4", + "version": "2.1.0", "description": "The tracer package for the Powertools for AWS Lambda (TypeScript) library", "author": { "name": "Amazon Web Services", @@ -32,10 +32,10 @@ "license": "MIT-0", "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing", - "@aws-sdk/client-dynamodb": "^3.549.0", - "@aws-sdk/client-xray": "^3.549.0", + "@aws-sdk/client-dynamodb": "^3.554.0", + "@aws-sdk/client-xray": "^3.554.0", "@types/promise-retry": "^1.1.6", - "aws-sdk": "^2.1595.0", + "aws-sdk": "^2.1599.0", "axios": "^1.6.8", "promise-retry": "^2.0.1" }, @@ -93,7 +93,7 @@ "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" }, "dependencies": { - "@aws-lambda-powertools/commons": "^2.0.4", + "@aws-lambda-powertools/commons": "^2.1.0", "aws-xray-sdk-core": "^3.6.0" }, "keywords": [ diff --git a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts index 0dfeb15b79..7d00633067 100644 --- a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts @@ -75,6 +75,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { }, { nameSuffix: 'AllFlagsOn', + outputFormat: 'ESM', } ); testTable.grantWriteData(fnAllFlagsEnabled); @@ -95,6 +96,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { }, { nameSuffix: 'NoCaptureErrOrResp', + outputFormat: 'ESM', } ); testTable.grantWriteData(fnNoCaptureErrorOrResponse); @@ -114,6 +116,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { }, { nameSuffix: 'TracerDisabled', + outputFormat: 'ESM', } ); testTable.grantWriteData(fnTracerDisabled); @@ -133,6 +136,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { }, { nameSuffix: 'CaptureResponseOff', + outputFormat: 'ESM', } ); testTable.grantWriteData(fnCaptureResponseOff); diff --git a/packages/tracer/tests/e2e/allFeatures.middy.test.ts b/packages/tracer/tests/e2e/allFeatures.middy.test.ts index 23d2fdfc23..b9488bf6ed 100644 --- a/packages/tracer/tests/e2e/allFeatures.middy.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.middy.test.ts @@ -75,6 +75,7 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { }, { nameSuffix: 'AllFlagsOn', + outputFormat: 'ESM', } ); testTable.grantWriteData(fnAllFlagsEnabled); @@ -95,6 +96,7 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { }, { nameSuffix: 'NoCaptureErrOrResp', + outputFormat: 'ESM', } ); testTable.grantWriteData(fnNoCaptureErrorOrResponse); @@ -114,6 +116,7 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { }, { nameSuffix: 'TracerDisabled', + outputFormat: 'ESM', } ); testTable.grantWriteData(fnTracerDisabled); @@ -133,6 +136,7 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { }, { nameSuffix: 'CaptureResponseOff', + outputFormat: 'ESM', } ); testTable.grantWriteData(fnCaptureResponseOff); diff --git a/packages/tracer/tests/helpers/resources.ts b/packages/tracer/tests/helpers/resources.ts index edd4faa769..7acd9730db 100644 --- a/packages/tracer/tests/helpers/resources.ts +++ b/packages/tracer/tests/helpers/resources.ts @@ -1,4 +1,7 @@ -import type { TestStack } from '@aws-lambda-powertools/testing-utils'; +import { + getRuntimeKey, + type TestStack, +} from '@aws-lambda-powertools/testing-utils'; import type { ExtraTestProps, TestNodejsFunctionProps, @@ -12,6 +15,9 @@ class TracerTestNodejsFunction extends TestNodejsFunction { props: TestNodejsFunctionProps, extraProps: ExtraTestProps ) { + const isEsm = extraProps.outputFormat === 'ESM'; + const isNodejs16x = getRuntimeKey() === 'nodejs16x'; + super( scope, { @@ -27,6 +33,18 @@ class TracerTestNodejsFunction extends TestNodejsFunction { ), ...props.environment, }, + /** + * For Node.js 16.x, we need to set `externalModules` to an empty array + * so that the `aws-sdk` is bundled with the function. + * + * @see https://github.com/aws/aws-sdk-js-v3/issues/3230#issuecomment-1561973247 + */ + bundling: { + ...(isEsm && + isNodejs16x && { + externalModules: [], + }), + }, }, extraProps ); diff --git a/tsconfig.json b/tsconfig.json index 06b00be287..c801ef543c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -44,5 +44,8 @@ { "path": "./packages/testing" }, + { + "path": "./packages/parser" + } ] } \ No newline at end of file