diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..df74662 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,163 @@ +# Lambda Starter Project Instructions + +## Project Overview + +This is an AWS Lambda serverless starter project written in **Node.js** and **TypeScript**. The project uses the **AWS CDK** for infrastructure as code. The project uses **Jest** for unit tests of the infrastructure and application code. + +--- + +## Technology Stack + +Each pattern project uses the following technology stack: + +- **Language:** TypeScript +- **Platform:** AWS Lambda +- **Runtime:** Node.js 24+ +- **AWS SDK:** v3 (modular packages) +- **Testing:** Jest +- **Linting/Formatting:** ESLint + Prettier +- **Validation:** Zod +- **Logging:** Pino + Pino-Lambda +- **Package Manager:** npm +- **Infrastructure:** AWS CDK +- **DevOps:** GitHub Actions + +--- + +## Project Structure + +The project follows a consistent directory and file structure to promote maintainability and scalability. Below is the high-level structure: + +``` +/docs # Project documentation + README.md # Documentation table of contents + +/src + /handlers + get-task.ts # Lambda handler + get-task.test.ts # Unit tests for get-task + /services + task-service.ts # Business logic + task-service.test.ts # Unit tests for task-service + /models + task.ts # Task data model + /utils + dynamodb-client.ts # AWS SDK client for DynamoDB + config.ts # App configuration helper + response.ts # Helper for formatting Lambda responses + response.test.ts + +/infrastructure + /stacks + lambda-stack.ts # CDK stack for Lambdas + lambda-stack.test.ts # Unit tests for lambda-stack + /utils + config.ts # CDK config helper + config.test.ts # Unit tests for CDK config helper + app.ts # CDK app entry point + cdk.json # CDK config + jest.config.ts # CDK Jest config + jest.setup.ts # CDK Jest setup + package.json # CDK NPM package config + README.md # CDK README + tsconfig.json # CDK TypeScript config + +.editorconfig # Editor config +.gitignore # Git ignore rules +.nvmrc # Node version manager config +.prettierrc # Prettier config +eslint.config.mjs # ESLint config +jest.config.ts # App Jest config +jest.setup.ts # App Jest setup +package.json # App NPM package config +README.md # Project README +tsconfig.json # Project TypeScript config +``` + +--- + +## Source Code Guidelines + +The project follows best practices for source code organization, naming conventions, and coding standards. Below are the key guidelines: + +- Use **TypeScript** for all source and infrastructure code. +- Use arrow functions for defining functions. +- Use path aliases for cleaner imports (e.g., `@/utils`, `@/models`). +- Handlers parse input, call services, and return responses reside in `/handlers`. +- Core business logic should reside in `/services`. +- Create types and interfaces in `/models` for data structures and DTOs. +- Create reusable utilities in `/utils` (e.g., AWS clients, response helpers, config helpers, logging). +- Validate configuration and input data with **Zod**. +- Organize import statements: external packages first, then internal modules. +- Use async/await for asynchronous operations. +- Handle errors gracefully and return meaningful error responses. +- Document functions and modules with JSDoc comments. + +### Source Code Commands & Scripts + +- Use `npm run build` to compile TypeScript. +- Use `npm run test` to run tests. +- Use `npm run test:coverage` to run tests with coverage report. +- Use `npm run lint` to run ESLint. +- Use `npm run lint:fix` to fix ESLint issues. +- Use `npm run format` to run Prettier to format code. +- Use `npm run format:check` to check code formatting with Prettier. + +--- + +## Unit Testing Guidelines + +The project includes comprehensive unit tests for both application and infrastructure code. Below are the key guidelines for writing unit tests: + +- Use the **Jest** testing framework. +- Place test files next to the source file, with `.test.ts` suffix. +- Use `describe` and `it` blocks for organization. +- Use `beforeEach` for setup and `afterEach` for cleanup. +- Use `expect` assertions for results. +- Mock dependencies to isolate the component under test. +- Mock external calls (e.g., AWS SDK, databases). +- Structure your tests using the Arrange-Act-Assert pattern: + - **Arrange:** Set up the test environment, including any necessary mocks and test data. + - **Act:** Execute the function or service being tested. + - **Assert:** Verify that the results are as expected. + - Add comments to separate these sections for clarity. + +--- + +## AWS CDK Guidelines + +The project uses AWS CDK for infrastructure as code. Below are the key guidelines for using AWS CDK: + +- Design philosophy prioritizes cost efficiency, scalability, and security. +- Self-contained AWS CDK project for infrastructure as code. +- Use **TypeScript** for all infrastructure code. +- Use **AWS CDK v2**. +- Define one CDK stack per major grouping of resources (e.g., lambda stack, data stack). +- Never commit secrets or hardcoded credentials. +- Use **.env** for local development configuration, but do not commit to source control. +- Use **.env.example** to document required environment variables. +- Prefix environment variables with `CDK_` to avoid conflicts. +- Use **AWS SSM Parameter Store** for secure configuration. +- All CDK resources must be tagged for cost allocation and management: + - `App`: Application name + - `Env`: Environment (e.g., dev, qat, prd) + - `OU`: Organizational Unit, e.g. `leanstacks` or `shared-services` + - `Owner`: Team or individual responsible +- Tag all CDK resources appropriately (`App`, `Env`, `OU`, `Owner`). + +### DynamoDB Tables + +- Prefer using single-table design where feasible. +- Use composite primary keys (partition key + sort key) for efficient querying. + +### Lambda Functions + +- Use **NodejsFunction** from `aws-cdk/aws-lambda-nodejs` to build Lambdas with automatic TypeScript transpilation. + +### AWS CDK Commands & Scripts + +- Use `npm run build` to compile TypeScript. +- Use `npm run test` to run tests. +- Use `npm run test:coverage` to run tests with coverage report. +- Use `npm run synth` to synthesize CDK stacks. +- Use `npm run cdk ` to run CDK commands (e.g., deploy, diff). diff --git a/.github/instructions/cdk.instructions.md b/.github/instructions/cdk.instructions.md deleted file mode 100644 index fa49a06..0000000 --- a/.github/instructions/cdk.instructions.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -applyTo: 'infrastructure/**' ---- - -# AWS CDK Guidelines - ---- - -## General Principles - -- Design philosophy prioritizes cost efficiency, scalability, and security. -- Self-contained AWS CDK project for infrastructure as code. -- Use **TypeScript** for all infrastructure code. -- Use **AWS CDK v2**. -- Define one CDK stack per major grouping of resources (e.g., lambda stack, data stack). -- Never commit secrets or hardcoded credentials. -- Use **.env** for local development configuration, but do not commit to source control. -- Use **.env.example** to document required environment variables. -- Prefix environment variables with `CDK_` to avoid conflicts. -- Use **AWS SSM Parameter Store** for secure configuration. -- All CDK resources must be tagged for cost allocation and management: - - `App`: Application name - - `Env`: Environment (e.g., dev, qat, prd) - - `OU`: Organizational Unit, e.g. `leanstacks` or `shared-services` - - `Owner`: Team or individual responsible -- Tag all CDK resources appropriately (`App`, `Env`, `OU`, `Owner`). - ---- - -## DynamoDB Tables - -- Prefer using single-table design where feasible. -- Use composite primary keys (partition key + sort key) for efficient querying. - ---- - -## Lambda Functions - -- Use **NodejsFunction** from `aws-cdk/aws-lambda-nodejs` to build Lambdas with automatic TypeScript transpilation. - ---- - -## Commands & Scripts - -- Use `npm run build` to compile TypeScript. -- Use `npm run test` to run tests. -- Use `npm run synth` to synthesize CDK stacks. -- Use `npm run cdk ` to run CDK commands (e.g., deploy, diff). diff --git a/.github/instructions/main.instructions.md b/.github/instructions/main.instructions.md deleted file mode 100644 index 7250e2e..0000000 --- a/.github/instructions/main.instructions.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -applyTo: '**' ---- - -# Main Project Guidelines - -## Project Overview - -This is an AWS Lambda serverless starter project written in **Node.js** and **TypeScript**. The project uses the **AWS CDK** for infrastructure as code. The project uses **Jest** for unit tests of the infrastructure and application code. - ---- - -## Technology Stack - -This project uses the following technologies: - -- **Language:** TypeScript -- **Platform:** AWS Lambda -- **Runtime:** Node.js 24+ -- **AWS SDK:** v3 (modular packages) -- **Testing:** Jest -- **Linting/Formatting:** ESLint + Prettier -- **Validation:** Zod -- **Logging:** Pino + Pino-Lambda -- **Package Manager:** npm -- **Infrastructure:** AWS CDK -- **DevOps:** GitHub Actions - ---- - -## Project Structure - -``` -/docs # Project documentation - README.md # Documentation table of contents - -/src - /handlers - get-task.ts # Lambda handler - get-task.test.ts # Unit tests for get-task - /services - task-service.ts # Business logic - task-service.test.ts # Unit tests for task-service - /models - task.ts # Task data model - /utils - dynamodb-client.ts # AWS SDK client for DynamoDB - config.ts # App configuration helper - response.ts # Helper for formatting Lambda responses - response.test.ts - -/infrastructure - /stacks - lambda-stack.ts # CDK stack for Lambdas - /utils - config.ts # CDK config helper - app.ts # CDK app entry point - cdk.json # CDK config - jest.config.ts # CDK Jest config - jest.setup.ts # CDK Jest setup - package.json # CDK NPM package config - README.md # CDK README - tsconfig.json # CDK TypeScript config - -.editorconfig # Editor config -.gitignore # Git ignore rules -.nvmrc # Node version manager config -.prettierrc # Prettier config -eslint.config.mjs # ESLint config -jest.config.ts # App Jest config -jest.setup.ts # App Jest setup -package.json # App NPM package config -README.md # Project README -tsconfig.json # Project TypeScript config -``` - ---- - -## Environments - -- Environments are: `dev`, `qat`, and `prd`. -- Each environment has its own AWS account. diff --git a/.github/instructions/src.instructions.md b/.github/instructions/src.instructions.md deleted file mode 100644 index 2dcb44c..0000000 --- a/.github/instructions/src.instructions.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -applyTo: 'src/**/*.ts,!src/**/*.test.ts,!src/**/*.spec.ts' ---- - -# Source Code Guidelines - -- Use **TypeScript** for all source and infrastructure code. -- Use arrow functions for defining functions. -- Use path aliases for cleaner imports (e.g., `@utils`, `@models`). -- Handlers parse input, call services, and return responses reside in `/handlers`. -- Core business logic should reside in `/services`. -- Create types and interfaces in `/models` for data structures and DTOs. -- Create reusable utilities in `/utils` (e.g., AWS clients, response helpers, config helpers, logging). -- Validate configuration and input data with **Zod**. -- Organize import statements: external packages first, then internal modules. -- Use async/await for asynchronous operations. -- Handle errors gracefully and return meaningful error responses. -- Document functions and modules with JSDoc comments. - -# Commands & Scripts - -- Use `npm run build` to compile TypeScript. -- Use `npm run test` to run tests. -- Use `npm run test:coverage` to run tests with coverage report. -- Use `npm run lint` to run ESLint. -- Use `npm run lint:fix` to fix ESLint issues. -- Use `npm run format` to run Prettier to format code. -- Use `npm run format:check` to check code formatting with Prettier. diff --git a/.github/instructions/test.instructions.md b/.github/instructions/test.instructions.md deleted file mode 100644 index bbd3ebe..0000000 --- a/.github/instructions/test.instructions.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -applyTo: '**/*.test.ts,**/*.spec.ts' ---- - -# Unit Testing Guidelines - -- Use the **Jest** testing framework. -- Place test files next to the source file, with `.test.ts` suffix. -- Use `describe` and `it` blocks for organization. -- Use `beforeEach` for setup and `afterEach` for cleanup. -- Use `expect` assertions for results. -- Mock dependencies to isolate the component under test. -- Mock external calls (e.g., AWS SDK, databases). -- Structure your tests using the Arrange-Act-Assert pattern: - - **Arrange:** Set up the test environment, including any necessary mocks and test data. - - **Act:** Execute the function or service being tested. - - **Assert:** Verify that the results are as expected. - - Add comments to separate these sections for clarity. - -# Commands & Scripts - -- Use `npm run test` to run tests. -- Use `npm run test:coverage` to run tests with coverage report. -- Use `npm run lint` to run ESLint. -- Use `npm run lint:fix` to fix ESLint issues. -- Use `npm run format` to run Prettier to format code. -- Use `npm run format:check` to check code formatting with Prettier. diff --git a/infrastructure/jest.config.ts b/infrastructure/jest.config.ts index 7fe4149..1f9e1b8 100644 --- a/infrastructure/jest.config.ts +++ b/infrastructure/jest.config.ts @@ -7,6 +7,7 @@ const config: Config = { testMatch: ['**/*.test.ts'], collectCoverageFrom: [ '**/*.ts', + '!app.ts', '!**/*.test.ts', '!**/*.spec.ts', '!**/node_modules/**', diff --git a/infrastructure/package-lock.json b/infrastructure/package-lock.json index 9347a7a..936b94b 100644 --- a/infrastructure/package-lock.json +++ b/infrastructure/package-lock.json @@ -16,9 +16,9 @@ }, "devDependencies": { "@types/jest": "30.0.0", - "@types/node": "25.0.2", - "aws-cdk": "2.1100.0", - "esbuild": "0.27.1", + "@types/node": "25.0.3", + "aws-cdk": "2.1100.1", + "esbuild": "0.27.2", "jest": "30.2.0", "rimraf": "6.1.2", "ts-jest": "29.4.6", @@ -105,6 +105,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -629,9 +630,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", - "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -646,9 +647,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", - "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -663,9 +664,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", - "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -680,9 +681,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", - "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -697,9 +698,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", - "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -714,9 +715,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", - "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -731,9 +732,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", - "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -748,9 +749,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", - "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -765,9 +766,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", - "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -782,9 +783,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", - "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -799,9 +800,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", - "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -816,9 +817,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", - "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -833,9 +834,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", - "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -850,9 +851,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", - "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -867,9 +868,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", - "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -884,9 +885,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", - "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -901,9 +902,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", - "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -918,9 +919,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", - "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "cpu": [ "arm64" ], @@ -935,9 +936,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", - "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -952,9 +953,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", - "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "cpu": [ "arm64" ], @@ -969,9 +970,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", - "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -986,9 +987,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", - "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "cpu": [ "arm64" ], @@ -1003,9 +1004,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", - "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -1020,9 +1021,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", - "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -1037,9 +1038,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", - "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -1054,9 +1055,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", - "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -1717,11 +1718,12 @@ } }, "node_modules/@types/node": { - "version": "25.0.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz", - "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==", + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2129,9 +2131,9 @@ } }, "node_modules/aws-cdk": { - "version": "2.1100.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1100.0.tgz", - "integrity": "sha512-EadIbrhBodY6Sl+7ujTgZGCyUdcDpbWdDEqq9HsAtYW4CiB/Idu30O7mwITXCqiMAGreko/clMttVh/XfgEbJA==", + "version": "2.1100.1", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1100.1.tgz", + "integrity": "sha512-q2poFrQh90TK6eqeI0zznA8r1JkDI63WVOSqC7gFGo6qjQjAnvFk/utxHoNRgAC0RL0CLd19uCcHh3jfX9NiSg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2653,6 +2655,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -2914,7 +2917,8 @@ "version": "10.4.4", "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.4.tgz", "integrity": "sha512-lP0qC1oViYf1cutHo9/KQ8QL637f/W29tDmv/6sy35F5zs+MD9f66nbAAIjicwc7fwyuF3rkg6PhZh4sfvWIpA==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/convert-source-map": { "version": "2.0.0", @@ -3065,9 +3069,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", - "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3078,32 +3082,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.1", - "@esbuild/android-arm": "0.27.1", - "@esbuild/android-arm64": "0.27.1", - "@esbuild/android-x64": "0.27.1", - "@esbuild/darwin-arm64": "0.27.1", - "@esbuild/darwin-x64": "0.27.1", - "@esbuild/freebsd-arm64": "0.27.1", - "@esbuild/freebsd-x64": "0.27.1", - "@esbuild/linux-arm": "0.27.1", - "@esbuild/linux-arm64": "0.27.1", - "@esbuild/linux-ia32": "0.27.1", - "@esbuild/linux-loong64": "0.27.1", - "@esbuild/linux-mips64el": "0.27.1", - "@esbuild/linux-ppc64": "0.27.1", - "@esbuild/linux-riscv64": "0.27.1", - "@esbuild/linux-s390x": "0.27.1", - "@esbuild/linux-x64": "0.27.1", - "@esbuild/netbsd-arm64": "0.27.1", - "@esbuild/netbsd-x64": "0.27.1", - "@esbuild/openbsd-arm64": "0.27.1", - "@esbuild/openbsd-x64": "0.27.1", - "@esbuild/openharmony-arm64": "0.27.1", - "@esbuild/sunos-x64": "0.27.1", - "@esbuild/win32-arm64": "0.27.1", - "@esbuild/win32-ia32": "0.27.1", - "@esbuild/win32-x64": "0.27.1" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/escalade": { @@ -3614,6 +3618,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -5362,6 +5367,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -5437,6 +5443,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/infrastructure/package.json b/infrastructure/package.json index 4df1a84..f729fb3 100644 --- a/infrastructure/package.json +++ b/infrastructure/package.json @@ -21,9 +21,9 @@ }, "devDependencies": { "@types/jest": "30.0.0", - "@types/node": "25.0.2", - "aws-cdk": "2.1100.0", - "esbuild": "0.27.1", + "@types/node": "25.0.3", + "aws-cdk": "2.1100.1", + "esbuild": "0.27.2", "jest": "30.2.0", "rimraf": "6.1.2", "ts-jest": "29.4.6", diff --git a/infrastructure/stacks/lambda-stack.ts b/infrastructure/stacks/lambda-stack.ts index 71d7eeb..69346a0 100644 --- a/infrastructure/stacks/lambda-stack.ts +++ b/infrastructure/stacks/lambda-stack.ts @@ -104,7 +104,7 @@ export class LambdaStack extends cdk.Stack { sourceMap: true, }, loggingFormat: lambda.LoggingFormat.JSON, - applicationLogLevelV2: lambda.ApplicationLogLevel.INFO, + applicationLogLevelV2: lambda.ApplicationLogLevel.DEBUG, systemLogLevelV2: lambda.SystemLogLevel.INFO, logGroup: new logs.LogGroup(this, 'ListTasksFunctionLogGroup', { logGroupName: `/aws/lambda/${props.appName}-list-tasks-${props.envName}`, @@ -136,7 +136,7 @@ export class LambdaStack extends cdk.Stack { sourceMap: true, }, loggingFormat: lambda.LoggingFormat.JSON, - applicationLogLevelV2: lambda.ApplicationLogLevel.INFO, + applicationLogLevelV2: lambda.ApplicationLogLevel.DEBUG, systemLogLevelV2: lambda.SystemLogLevel.INFO, logGroup: new logs.LogGroup(this, 'GetTaskFunctionLogGroup', { logGroupName: `/aws/lambda/${props.appName}-get-task-${props.envName}`, @@ -168,7 +168,7 @@ export class LambdaStack extends cdk.Stack { sourceMap: true, }, loggingFormat: lambda.LoggingFormat.JSON, - applicationLogLevelV2: lambda.ApplicationLogLevel.INFO, + applicationLogLevelV2: lambda.ApplicationLogLevel.DEBUG, systemLogLevelV2: lambda.SystemLogLevel.INFO, logGroup: new logs.LogGroup(this, 'CreateTaskFunctionLogGroup', { logGroupName: `/aws/lambda/${props.appName}-create-task-${props.envName}`, @@ -200,7 +200,7 @@ export class LambdaStack extends cdk.Stack { sourceMap: true, }, loggingFormat: lambda.LoggingFormat.JSON, - applicationLogLevelV2: lambda.ApplicationLogLevel.INFO, + applicationLogLevelV2: lambda.ApplicationLogLevel.DEBUG, systemLogLevelV2: lambda.SystemLogLevel.INFO, logGroup: new logs.LogGroup(this, 'UpdateTaskFunctionLogGroup', { logGroupName: `/aws/lambda/${props.appName}-update-task-${props.envName}`, @@ -232,7 +232,7 @@ export class LambdaStack extends cdk.Stack { sourceMap: true, }, loggingFormat: lambda.LoggingFormat.JSON, - applicationLogLevelV2: lambda.ApplicationLogLevel.INFO, + applicationLogLevelV2: lambda.ApplicationLogLevel.DEBUG, systemLogLevelV2: lambda.SystemLogLevel.INFO, logGroup: new logs.LogGroup(this, 'DeleteTaskFunctionLogGroup', { logGroupName: `/aws/lambda/${props.appName}-delete-task-${props.envName}`, diff --git a/jest.config.ts b/jest.config.ts index 1867ef4..364b963 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -10,6 +10,8 @@ const config: Config = { coverageReporters: ['json', 'json-summary', 'lcov', 'text', 'clover'], moduleFileExtensions: ['ts', 'js', 'json'], moduleNameMapper: { + '^@/(.+)\\.js$': '/src/$1.ts', + '^@/(.*)$': '/src/$1', '^(\\.{1,2}/.*)\\.js$': '$1', }, transform: { diff --git a/package-lock.json b/package-lock.json index 4c0e2f7..d37c715 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,10 @@ "version": "0.2.0-alpha", "license": "MIT", "dependencies": { - "@aws-sdk/client-dynamodb": "3.952.0", - "@aws-sdk/client-lambda": "3.952.0", - "@aws-sdk/client-sns": "3.952.0", - "@aws-sdk/lib-dynamodb": "3.952.0", + "@aws-sdk/client-dynamodb": "3.953.0", + "@aws-sdk/client-lambda": "3.953.0", + "@aws-sdk/client-sns": "3.953.0", + "@aws-sdk/lib-dynamodb": "3.953.0", "pino": "10.1.0", "pino-lambda": "4.4.1", "zod": "4.2.1" @@ -20,7 +20,7 @@ "devDependencies": { "@types/aws-lambda": "8.10.159", "@types/jest": "30.0.0", - "@types/node": "25.0.2", + "@types/node": "25.0.3", "@typescript-eslint/eslint-plugin": "8.50.0", "@typescript-eslint/parser": "8.50.0", "eslint": "9.39.2", @@ -176,53 +176,53 @@ } }, "node_modules/@aws-sdk/client-dynamodb": { - "version": "3.952.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.952.0.tgz", - "integrity": "sha512-Do6aCz5ZpVmcEjr3j7A5kIm+aNucViIQsf42g5CLIvxgB8RQCudp3TKHLfNGoRHoR8ycjppboSlJpUPlu/iC3A==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.953.0.tgz", + "integrity": "sha512-QeSFxXgRjpr8M2wiLUsgg+mXEDtdhcuMnBWbXyjqUwca38pLEFJzJdFyOGul9RoQ2ICseuAy2/RZt0Ri1UgeZQ==", "license": "Apache-2.0", "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/credential-provider-node": "3.952.0", - "@aws-sdk/dynamodb-codec": "3.947.0", - "@aws-sdk/middleware-endpoint-discovery": "3.936.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/credential-provider-node": "3.953.0", + "@aws-sdk/dynamodb-codec": "3.953.0", + "@aws-sdk/middleware-endpoint-discovery": "3.953.0", + "@aws-sdk/middleware-host-header": "3.953.0", + "@aws-sdk/middleware-logger": "3.953.0", + "@aws-sdk/middleware-recursion-detection": "3.953.0", + "@aws-sdk/middleware-user-agent": "3.953.0", + "@aws-sdk/region-config-resolver": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-endpoints": "3.953.0", + "@aws-sdk/util-user-agent-browser": "3.953.0", + "@aws-sdk/util-user-agent-node": "3.953.0", + "@smithy/config-resolver": "^4.4.4", + "@smithy/core": "^3.19.0", + "@smithy/fetch-http-handler": "^5.3.7", + "@smithy/hash-node": "^4.2.6", + "@smithy/invalid-dependency": "^4.2.6", + "@smithy/middleware-content-length": "^4.2.6", + "@smithy/middleware-endpoint": "^4.3.15", + "@smithy/middleware-retry": "^4.4.15", + "@smithy/middleware-serde": "^4.2.7", + "@smithy/middleware-stack": "^4.2.6", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/node-http-handler": "^4.4.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/smithy-client": "^4.10.0", + "@smithy/types": "^4.10.0", + "@smithy/url-parser": "^4.2.6", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "@smithy/util-defaults-mode-browser": "^4.3.14", + "@smithy/util-defaults-mode-node": "^4.2.17", + "@smithy/util-endpoints": "^3.2.6", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-retry": "^4.2.6", "@smithy/util-utf8": "^4.2.0", - "@smithy/util-waiter": "^4.2.5", + "@smithy/util-waiter": "^4.2.6", "tslib": "^2.6.2" }, "engines": { @@ -230,54 +230,54 @@ } }, "node_modules/@aws-sdk/client-lambda": { - "version": "3.952.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.952.0.tgz", - "integrity": "sha512-F0cK/RX1qs9NLZctGokrKe6AlK4YgiHqYaYBo32d+xT+EZ4EpNuOIWNZ4Rgq+78tG+Y9DT98Nwwl9lejYQtrog==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.953.0.tgz", + "integrity": "sha512-Bsm5arLOjkrhGQzycmswnVWwJANd447h5e/TRqyYd07Zr6LIhylcACOeyJIoz7z1ffXTCTj8iLGE7Zac4g0mSQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/credential-provider-node": "3.952.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/eventstream-serde-browser": "^4.2.5", - "@smithy/eventstream-serde-config-resolver": "^4.3.5", - "@smithy/eventstream-serde-node": "^4.2.5", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/credential-provider-node": "3.953.0", + "@aws-sdk/middleware-host-header": "3.953.0", + "@aws-sdk/middleware-logger": "3.953.0", + "@aws-sdk/middleware-recursion-detection": "3.953.0", + "@aws-sdk/middleware-user-agent": "3.953.0", + "@aws-sdk/region-config-resolver": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-endpoints": "3.953.0", + "@aws-sdk/util-user-agent-browser": "3.953.0", + "@aws-sdk/util-user-agent-node": "3.953.0", + "@smithy/config-resolver": "^4.4.4", + "@smithy/core": "^3.19.0", + "@smithy/eventstream-serde-browser": "^4.2.6", + "@smithy/eventstream-serde-config-resolver": "^4.3.6", + "@smithy/eventstream-serde-node": "^4.2.6", + "@smithy/fetch-http-handler": "^5.3.7", + "@smithy/hash-node": "^4.2.6", + "@smithy/invalid-dependency": "^4.2.6", + "@smithy/middleware-content-length": "^4.2.6", + "@smithy/middleware-endpoint": "^4.3.15", + "@smithy/middleware-retry": "^4.4.15", + "@smithy/middleware-serde": "^4.2.7", + "@smithy/middleware-stack": "^4.2.6", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/node-http-handler": "^4.4.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/smithy-client": "^4.10.0", + "@smithy/types": "^4.10.0", + "@smithy/url-parser": "^4.2.6", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/util-stream": "^4.5.6", + "@smithy/util-defaults-mode-browser": "^4.3.14", + "@smithy/util-defaults-mode-node": "^4.2.17", + "@smithy/util-endpoints": "^3.2.6", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-retry": "^4.2.6", + "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", - "@smithy/util-waiter": "^4.2.5", + "@smithy/util-waiter": "^4.2.6", "tslib": "^2.6.2" }, "engines": { @@ -285,48 +285,48 @@ } }, "node_modules/@aws-sdk/client-sns": { - "version": "3.952.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sns/-/client-sns-3.952.0.tgz", - "integrity": "sha512-8PIrjbrzBEBECtgUXLXU6dGONixCHOfFLiwqCgBlsbphRRWo2N3DF4N8p+Svvs/rPgin9I/r1hZEqP3UpQ6jQg==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sns/-/client-sns-3.953.0.tgz", + "integrity": "sha512-tlmq3g9WxxMBZsOTak22gs8gXCXUCtKMZvl2CJrB6/kPm0CTqquKli/S7XmpWFj8BudUmf48R+Cc23AkRCvtPA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/credential-provider-node": "3.952.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/credential-provider-node": "3.953.0", + "@aws-sdk/middleware-host-header": "3.953.0", + "@aws-sdk/middleware-logger": "3.953.0", + "@aws-sdk/middleware-recursion-detection": "3.953.0", + "@aws-sdk/middleware-user-agent": "3.953.0", + "@aws-sdk/region-config-resolver": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-endpoints": "3.953.0", + "@aws-sdk/util-user-agent-browser": "3.953.0", + "@aws-sdk/util-user-agent-node": "3.953.0", + "@smithy/config-resolver": "^4.4.4", + "@smithy/core": "^3.19.0", + "@smithy/fetch-http-handler": "^5.3.7", + "@smithy/hash-node": "^4.2.6", + "@smithy/invalid-dependency": "^4.2.6", + "@smithy/middleware-content-length": "^4.2.6", + "@smithy/middleware-endpoint": "^4.3.15", + "@smithy/middleware-retry": "^4.4.15", + "@smithy/middleware-serde": "^4.2.7", + "@smithy/middleware-stack": "^4.2.6", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/node-http-handler": "^4.4.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/smithy-client": "^4.10.0", + "@smithy/types": "^4.10.0", + "@smithy/url-parser": "^4.2.6", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "@smithy/util-defaults-mode-browser": "^4.3.14", + "@smithy/util-defaults-mode-node": "^4.2.17", + "@smithy/util-endpoints": "^3.2.6", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-retry": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -335,47 +335,47 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.948.0.tgz", - "integrity": "sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.953.0.tgz", + "integrity": "sha512-WU8XtORGnhy1JjTLflcGcNGU329pmwl/2claTVGp7vrgUKyreQV9Ke3BkMuUPYLOGO/Znstzc1VbUT9ORH3+WQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/middleware-host-header": "3.953.0", + "@aws-sdk/middleware-logger": "3.953.0", + "@aws-sdk/middleware-recursion-detection": "3.953.0", + "@aws-sdk/middleware-user-agent": "3.953.0", + "@aws-sdk/region-config-resolver": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-endpoints": "3.953.0", + "@aws-sdk/util-user-agent-browser": "3.953.0", + "@aws-sdk/util-user-agent-node": "3.953.0", + "@smithy/config-resolver": "^4.4.4", + "@smithy/core": "^3.19.0", + "@smithy/fetch-http-handler": "^5.3.7", + "@smithy/hash-node": "^4.2.6", + "@smithy/invalid-dependency": "^4.2.6", + "@smithy/middleware-content-length": "^4.2.6", + "@smithy/middleware-endpoint": "^4.3.15", + "@smithy/middleware-retry": "^4.4.15", + "@smithy/middleware-serde": "^4.2.7", + "@smithy/middleware-stack": "^4.2.6", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/node-http-handler": "^4.4.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/smithy-client": "^4.10.0", + "@smithy/types": "^4.10.0", + "@smithy/url-parser": "^4.2.6", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "@smithy/util-defaults-mode-browser": "^4.3.14", + "@smithy/util-defaults-mode-node": "^4.2.17", + "@smithy/util-endpoints": "^3.2.6", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-retry": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -384,22 +384,22 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.947.0.tgz", - "integrity": "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.953.0.tgz", + "integrity": "sha512-hnz4ukn+XupuotZcFAyCIX41QqEWSHc8zf4gN4l5qVP2M8YCyZkLLZ5t5LaWLJkUFbOUU2w4SuGpUp+5ddtSkw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@aws-sdk/xml-builder": "3.930.0", - "@smithy/core": "^3.18.7", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/signature-v4": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/xml-builder": "3.953.0", + "@smithy/core": "^3.19.0", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/property-provider": "^4.2.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/signature-v4": "^5.3.6", + "@smithy/smithy-client": "^4.10.0", + "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.5", + "@smithy/util-middleware": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -408,15 +408,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.947.0.tgz", - "integrity": "sha512-VR2V6dRELmzwAsCpK4GqxUi6UW5WNhAXS9F9AzWi5jvijwJo3nH92YNJUP4quMpgFZxJHEWyXLWgPjh9u0zYOA==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.953.0.tgz", + "integrity": "sha512-ahOxDxvaKUf99LU9iRmRtPW2HLmsR+ix2WyzRGl1PpLltiRLMbKJHj5Tu2xyOsgLxs8tefK9D1j0SUgPk8GJ6g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -424,20 +424,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.947.0.tgz", - "integrity": "sha512-inF09lh9SlHj63Vmr5d+LmwPXZc2IbK8lAruhOr3KLsZAIHEgHgGPXWDC2ukTEMzg0pkexQ6FOhXXad6klK4RA==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.953.0.tgz", + "integrity": "sha512-kYQEcJpJLiT+1ZdsQDMrIs2o3YydxdAIDdLUxbIwks1+WQz2sCr9b94ld19aMZ0rejosASO0J3dfY0lt3Tfl+Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/util-stream": "^4.5.6", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@smithy/fetch-http-handler": "^5.3.7", + "@smithy/node-http-handler": "^4.4.6", + "@smithy/property-provider": "^4.2.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/smithy-client": "^4.10.0", + "@smithy/types": "^4.10.0", + "@smithy/util-stream": "^4.5.7", "tslib": "^2.6.2" }, "engines": { @@ -445,24 +445,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.952.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.952.0.tgz", - "integrity": "sha512-N5B15SwzMkZ8/LLopNksTlPEWWZn5tbafZAUfMY5Xde4rSHGWmv5H/ws2M3P8L0X77E2wKnOJsNmu+GsArBreQ==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.953.0.tgz", + "integrity": "sha512-5cy6b3VntBvhRCanwohRtR0wbKKUd6JfIsnuXImB4JcZa2iMW5tDW0LhNangYZ9wrrM7sJol6cKHtUW9LNemFQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/credential-provider-env": "3.947.0", - "@aws-sdk/credential-provider-http": "3.947.0", - "@aws-sdk/credential-provider-login": "3.952.0", - "@aws-sdk/credential-provider-process": "3.947.0", - "@aws-sdk/credential-provider-sso": "3.952.0", - "@aws-sdk/credential-provider-web-identity": "3.952.0", - "@aws-sdk/nested-clients": "3.952.0", - "@aws-sdk/types": "3.936.0", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/credential-provider-env": "3.953.0", + "@aws-sdk/credential-provider-http": "3.953.0", + "@aws-sdk/credential-provider-login": "3.953.0", + "@aws-sdk/credential-provider-process": "3.953.0", + "@aws-sdk/credential-provider-sso": "3.953.0", + "@aws-sdk/credential-provider-web-identity": "3.953.0", + "@aws-sdk/nested-clients": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@smithy/credential-provider-imds": "^4.2.6", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -470,18 +470,18 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.952.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.952.0.tgz", - "integrity": "sha512-jL9zc+e+7sZeJrHzYKK9GOjl1Ktinh0ORU3cM2uRBi7fuH/0zV9pdMN8PQnGXz0i4tJaKcZ1lrE4V0V6LB9NQg==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.953.0.tgz", + "integrity": "sha512-Drf4v7uBKnU/nY/qgQD9ZA2zD5gjQEatxQajQHUN54erDSQ2rB5ApNuTnc2cyNwaxURnQvO1J72mwO6P5lIpsg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.952.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/nested-clients": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -489,22 +489,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.952.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.952.0.tgz", - "integrity": "sha512-pj7nidLrb3Dz9llcUPh6N0Yv1dBYTS9xJqi8u0kI8D5sn72HJMB+fIOhcDQVXXAw/dpVolOAH9FOAbog5JDAMg==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.953.0.tgz", + "integrity": "sha512-/3gUap/QwAV/U3RPStcSjdiksDboTdcz82qajfhURhtAe9iEdoKJy7vTYqg75eX7IjpgBwLGajt0URasUofrPg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.947.0", - "@aws-sdk/credential-provider-http": "3.947.0", - "@aws-sdk/credential-provider-ini": "3.952.0", - "@aws-sdk/credential-provider-process": "3.947.0", - "@aws-sdk/credential-provider-sso": "3.952.0", - "@aws-sdk/credential-provider-web-identity": "3.952.0", - "@aws-sdk/types": "3.936.0", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/credential-provider-env": "3.953.0", + "@aws-sdk/credential-provider-http": "3.953.0", + "@aws-sdk/credential-provider-ini": "3.953.0", + "@aws-sdk/credential-provider-process": "3.953.0", + "@aws-sdk/credential-provider-sso": "3.953.0", + "@aws-sdk/credential-provider-web-identity": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@smithy/credential-provider-imds": "^4.2.6", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -512,16 +512,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.947.0.tgz", - "integrity": "sha512-WpanFbHe08SP1hAJNeDdBDVz9SGgMu/gc0XJ9u3uNpW99nKZjDpvPRAdW7WLA4K6essMjxWkguIGNOpij6Do2Q==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.953.0.tgz", + "integrity": "sha512-YeqzqxzBzXnqdzn4pBuoXeCmWrXOLIu5owAowUc4dOvHmtlpXxB3beJdQ9g/Ky6fG6iaAPKO1vrCEoHNjM85Dw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -529,18 +529,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.952.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.952.0.tgz", - "integrity": "sha512-1CQdP5RzxeXuEfytbAD5TgreY1c9OacjtCdO8+n9m05tpzBABoNBof0hcjzw1dtrWFH7deyUgfwCl1TAN3yBWQ==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.953.0.tgz", + "integrity": "sha512-Kp/49vAJweixMo3q85eVpq1JO9KgKc6A+f8/8WDkN5CkMsfKQcGJZbO5JtuJfzar4pPmRKbgOeOP1qQsOmfyfw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.948.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/token-providers": "3.952.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/client-sso": "3.953.0", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/token-providers": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -548,17 +548,17 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.952.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.952.0.tgz", - "integrity": "sha512-5hJbfaZdHDAP8JlwplNbXJAat9Vv7L0AbTZzkbPIgjHhC3vrMf5r3a6I1HWFp5i5pXo7J45xyuf5uQGZJxJlCg==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.953.0.tgz", + "integrity": "sha512-V8SpGVtNjQZboHjb/GLM4L3/6O/AZJFtwJ8fiBaemKMTntl8haioZlQHDklWixR9s9zthOdFsCDs1+92ndUBRw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.952.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/nested-clients": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -566,15 +566,15 @@ } }, "node_modules/@aws-sdk/dynamodb-codec": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.947.0.tgz", - "integrity": "sha512-LSJQldMJs+UeZn8fFZW/vqL083Y/jEgttif4KJsTcTPSuauZCACzuouV//ZCEOc9YJKuh56iq9+CCXM+ZM0o9g==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.953.0.tgz", + "integrity": "sha512-VJDM2VvFb/VQxiRXmLw4sWCDqhAuYNzD5toANgwRazeZmCg0em6dtLdb97Q+4BT+MBXI4t1CS2xKaGtKOieNug==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@smithy/core": "^3.18.7", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.953.0", + "@smithy/core": "^3.19.0", + "@smithy/smithy-client": "^4.10.0", + "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, @@ -582,13 +582,13 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@aws-sdk/client-dynamodb": "^3.947.0" + "@aws-sdk/client-dynamodb": "^3.953.0" } }, "node_modules/@aws-sdk/endpoint-cache": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.893.0.tgz", - "integrity": "sha512-KSwTfyLZyNLszz5f/yoLC+LC+CRKpeJii/+zVAy7JUOQsKhSykiRUPYUx7o2Sdc4oJfqqUl26A/jSttKYnYtAA==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.953.0.tgz", + "integrity": "sha512-pz67DoHk5WNmvMuyNDiomUS2xo0mq6Z3TdfLJZlWVbSKi3h8hYxVQchJ2kzgTr6wu6zt3UBbtKV9yY1IBhKMVA==", "license": "Apache-2.0", "dependencies": { "mnemonist": "0.38.3", @@ -599,36 +599,36 @@ } }, "node_modules/@aws-sdk/lib-dynamodb": { - "version": "3.952.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.952.0.tgz", - "integrity": "sha512-NGN3a+FKOeMzJcdCG05XGlzDfp5EFt20kKPQCxNkz5nIwqAvhVkAtTHbVJihvc64h+XS4IfE8rrpoQNGLk+5yA==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.953.0.tgz", + "integrity": "sha512-oiCj2gZumRjhnv2RfI3oYgx0TFeollUt5JEzWjcCFbT2Ou9J2Ay1FEH9/xYGLxHu/NCGUVEAHdwuDsJcb09KHQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/util-dynamodb": "3.952.0", - "@smithy/core": "^3.18.7", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/util-dynamodb": "3.953.0", + "@smithy/core": "^3.19.0", + "@smithy/smithy-client": "^4.10.0", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "@aws-sdk/client-dynamodb": "^3.952.0" + "@aws-sdk/client-dynamodb": "^3.953.0" } }, "node_modules/@aws-sdk/middleware-endpoint-discovery": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.936.0.tgz", - "integrity": "sha512-wNJZ8PDw0eQK2x4z1q8JqiDvw9l9xd36EoklVT2CIBt8FnqGdrMGjAx93RRbH3G6Fmvwoe+D3VJXbWHBlhD0Bw==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.953.0.tgz", + "integrity": "sha512-/YKB1/OiWr7TwOfmkqzv8x1xgOpU71yciQTfsq6erB3dTQhdukPADt/CMJOhWFKC6Q1D5cDN8381nsGmnNuBVg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/endpoint-cache": "3.893.0", - "@aws-sdk/types": "3.936.0", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/endpoint-cache": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -636,14 +636,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", - "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.953.0.tgz", + "integrity": "sha512-jTGhfkONav+r4E6HLOrl5SzBqDmPByUYCkyB/c/3TVb8jX3wAZx8/q9bphKpCh+G5ARi3IdbSisgkZrJYqQ19Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -651,13 +651,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", - "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.953.0.tgz", + "integrity": "sha512-PlWdVYgcuptkIC0ZKqVUhWNtSHXJSx7U9V8J7dJjRmsXC40X7zpEycvrkzDMJjeTDGcCceYbyYAg/4X1lkcIMw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -665,15 +665,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.948.0.tgz", - "integrity": "sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.953.0.tgz", + "integrity": "sha512-cmIJx0gWeesUKK4YwgE+VQL3mpACr3/J24fbwnc1Z5tntC86b+HQFzU5vsBDw6lLwyD46dBgWdsXFh1jL+ZaFw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", + "@aws-sdk/types": "3.953.0", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -681,17 +681,17 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.947.0.tgz", - "integrity": "sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.953.0.tgz", + "integrity": "sha512-xZSU54cEHlIvRTUewyTAajb4WJPdbBd1mIaDYOzac07+vsfMuCREQ5CheQ3inoBfqske7QOX+mIjLpVV4azAnw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@smithy/core": "^3.18.7", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-endpoints": "3.953.0", + "@smithy/core": "^3.19.0", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -699,47 +699,47 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.952.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.952.0.tgz", - "integrity": "sha512-OtuirjxuOqZyDcI0q4WtoyWfkq3nSnbH41JwJQsXJefduWcww1FQe5TL1JfYCU7seUxHzK8rg2nFxUBuqUlZtg==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.953.0.tgz", + "integrity": "sha512-YEBI0b/4ezNkw5W4+RBRUoueFzqEPPrnkh/3LBqeJ7RWZTymBhxlpoLo6Gfxw9LxtDgv2vZ5K5rgC4/6Ly8ADg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/middleware-host-header": "3.953.0", + "@aws-sdk/middleware-logger": "3.953.0", + "@aws-sdk/middleware-recursion-detection": "3.953.0", + "@aws-sdk/middleware-user-agent": "3.953.0", + "@aws-sdk/region-config-resolver": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-endpoints": "3.953.0", + "@aws-sdk/util-user-agent-browser": "3.953.0", + "@aws-sdk/util-user-agent-node": "3.953.0", + "@smithy/config-resolver": "^4.4.4", + "@smithy/core": "^3.19.0", + "@smithy/fetch-http-handler": "^5.3.7", + "@smithy/hash-node": "^4.2.6", + "@smithy/invalid-dependency": "^4.2.6", + "@smithy/middleware-content-length": "^4.2.6", + "@smithy/middleware-endpoint": "^4.3.15", + "@smithy/middleware-retry": "^4.4.15", + "@smithy/middleware-serde": "^4.2.7", + "@smithy/middleware-stack": "^4.2.6", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/node-http-handler": "^4.4.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/smithy-client": "^4.10.0", + "@smithy/types": "^4.10.0", + "@smithy/url-parser": "^4.2.6", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "@smithy/util-defaults-mode-browser": "^4.3.14", + "@smithy/util-defaults-mode-node": "^4.2.17", + "@smithy/util-endpoints": "^3.2.6", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-retry": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -748,15 +748,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", - "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.953.0.tgz", + "integrity": "sha512-5MJgnsc+HLO+le0EK1cy92yrC7kyhGZSpaq8PcQvKs9qtXCXT5Tb6tMdkr5Y07JxYsYOV1omWBynvL6PWh08tQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@smithy/config-resolver": "^4.4.4", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -764,17 +764,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.952.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.952.0.tgz", - "integrity": "sha512-IpQVC9WOeXQlCEcFVNXWDIKy92CH1Az37u9K0H3DF/HT56AjhyDVKQQfHUy00nt7bHFe3u0K5+zlwErBeKy5ZA==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.953.0.tgz", + "integrity": "sha512-4FWWR3lDDuIftmCEWpGejfkJu2J1VG2MUktChQshADXABfVfM0fsT7OYlO7Sy106nOe9upE4uQTWXg9YT/6ssw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.952.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.953.0", + "@aws-sdk/nested-clients": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -782,12 +782,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", - "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.953.0.tgz", + "integrity": "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -795,9 +795,9 @@ } }, "node_modules/@aws-sdk/util-dynamodb": { - "version": "3.952.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.952.0.tgz", - "integrity": "sha512-Y+0s5ZCQ2IGN26YswJrlyHTfRZYfX/9o1K23a4U09jrS8a6HrNl4Q0jsnCVp0itpOP06L/HEfLNC6M7mPzVSGg==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.953.0.tgz", + "integrity": "sha512-WxDCCmpyJwAuBly+M7XYxdR9vLH69DjJT/8mWBeaxCcaZQO6W7l8PGGNMSHZQSMcWO5vhqOf0l29ZHdK3a63FA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -806,19 +806,19 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@aws-sdk/client-dynamodb": "^3.952.0" + "@aws-sdk/client-dynamodb": "^3.953.0" } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", - "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.953.0.tgz", + "integrity": "sha512-rjaS6jrFksopXvNg6YeN+D1lYwhcByORNlFuYesFvaQNtPOufbE5tJL4GJ3TMXyaY0uFR28N5BHHITPyWWfH/g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-endpoints": "^3.2.5", + "@aws-sdk/types": "3.953.0", + "@smithy/types": "^4.10.0", + "@smithy/url-parser": "^4.2.6", + "@smithy/util-endpoints": "^3.2.6", "tslib": "^2.6.2" }, "engines": { @@ -838,27 +838,27 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", - "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.953.0.tgz", + "integrity": "sha512-UF5NeqYesWuFao+u7LJvpV1SJCaLml5BtFZKUdTnNNMeN6jvV+dW/eQoFGpXF94RCqguX0XESmRuRRPQp+/rzQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@smithy/types": "^4.10.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.947.0.tgz", - "integrity": "sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.953.0.tgz", + "integrity": "sha512-HjJ0Nq/57kg0QCjt8mwBAK6C34njKezy9ItUNcrWITzrBZv7ikQtyqM1pindAIzgLaBb7ly/0kbJqMY+NPbcJA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/middleware-user-agent": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -874,12 +874,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.930.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", - "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.953.0.tgz", + "integrity": "sha512-Zmrj21jQ2OeOJGr9spPiN00aQvXa/WUqRXcTVENhrMt+OFoSOfDFpYhUj9NQ09QmQ8KMWFoWuWW6iKurNqLvAA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.10.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" }, @@ -3153,9 +3153,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz", - "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==", + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "dev": true, "license": "MIT", "peer": true, diff --git a/package.json b/package.json index e23b27c..92ff534 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "devDependencies": { "@types/aws-lambda": "8.10.159", "@types/jest": "30.0.0", - "@types/node": "25.0.2", + "@types/node": "25.0.3", "@typescript-eslint/eslint-plugin": "8.50.0", "@typescript-eslint/parser": "8.50.0", "eslint": "9.39.2", @@ -54,10 +54,10 @@ "typescript-eslint": "8.50.0" }, "dependencies": { - "@aws-sdk/client-dynamodb": "3.952.0", - "@aws-sdk/client-lambda": "3.952.0", - "@aws-sdk/client-sns": "3.952.0", - "@aws-sdk/lib-dynamodb": "3.952.0", + "@aws-sdk/client-dynamodb": "3.953.0", + "@aws-sdk/client-lambda": "3.953.0", + "@aws-sdk/client-sns": "3.953.0", + "@aws-sdk/lib-dynamodb": "3.953.0", "pino": "10.1.0", "pino-lambda": "4.4.1", "zod": "4.2.1" diff --git a/src/handlers/create-task.test.ts b/src/handlers/create-task.test.ts index d588ea8..c4817d6 100644 --- a/src/handlers/create-task.test.ts +++ b/src/handlers/create-task.test.ts @@ -27,6 +27,7 @@ jest.mock('../utils/logger', () => ({ info: mockLoggerInfo, warn: mockLoggerWarn, error: mockLoggerError, + debug: jest.fn(), }, })); @@ -136,10 +137,16 @@ describe('create-task handler', () => { expect(JSON.parse(result.body)).toEqual(mockTask); expect(mockCreateTask).toHaveBeenCalledTimes(1); expect(mockCreateTask).toHaveBeenCalledWith(requestBody); - expect(mockLoggerInfo).toHaveBeenCalledWith('[CreateTask] > handler', expect.any(Object)); expect(mockLoggerInfo).toHaveBeenCalledWith( - '[CreateTask] < handler - successfully created task', + expect.objectContaining({ + event: expect.any(Object), + context: expect.any(Object), + }), + '[CreateTaskHandler] > handler', + ); + expect(mockLoggerInfo).toHaveBeenCalledWith( expect.any(Object), + '[CreateTaskHandler] < handler - successfully created task', ); }); @@ -182,7 +189,7 @@ describe('create-task handler', () => { expect(result.statusCode).toBe(400); expect(JSON.parse(result.body)).toEqual({ message: 'Request body is required' }); expect(mockCreateTask).not.toHaveBeenCalled(); - expect(mockLoggerWarn).toHaveBeenCalledWith('[CreateTask] < handler - missing request body', expect.any(Object)); + expect(mockLoggerWarn).toHaveBeenCalledWith('[CreateTaskHandler] < handler - missing request body'); }); it('should return 400 when request body is not valid JSON', async () => { @@ -197,10 +204,7 @@ describe('create-task handler', () => { expect(result.statusCode).toBe(400); expect(JSON.parse(result.body)).toEqual({ message: 'Invalid JSON in request body' }); expect(mockCreateTask).not.toHaveBeenCalled(); - expect(mockLoggerWarn).toHaveBeenCalledWith( - '[CreateTask] < handler - invalid JSON in request body', - expect.any(Object), - ); + expect(mockLoggerWarn).toHaveBeenCalledWith('[CreateTaskHandler] < handler - invalid JSON in request body'); }); it('should return 400 when title is missing', async () => { @@ -221,7 +225,10 @@ describe('create-task handler', () => { expect(responseBody.message).toContain('Validation failed'); expect(responseBody.message).toContain('title'); expect(mockCreateTask).not.toHaveBeenCalled(); - expect(mockLoggerWarn).toHaveBeenCalledWith('[CreateTask] < handler - validation error', expect.any(Object)); + expect(mockLoggerWarn).toHaveBeenCalledWith( + expect.any(Object), + '[CreateTaskHandler] < handler - validation error', + ); }); it('should return 400 when title is empty', async () => { @@ -345,9 +352,8 @@ describe('create-task handler', () => { expect(JSON.parse(result.body)).toEqual({ message: 'Failed to create task' }); expect(mockCreateTask).toHaveBeenCalledTimes(1); expect(mockLoggerError).toHaveBeenCalledWith( - '[CreateTask] < handler - failed to create task', - mockError, - expect.any(Object), + expect.objectContaining({ error: mockError }), + '[CreateTaskHandler] < handler - failed to create task', ); }); diff --git a/src/handlers/create-task.ts b/src/handlers/create-task.ts index 6b559c2..fb5ebd0 100644 --- a/src/handlers/create-task.ts +++ b/src/handlers/create-task.ts @@ -2,10 +2,10 @@ import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda import { lambdaRequestTracker } from 'pino-lambda'; import { ZodError } from 'zod'; -import { CreateTaskDtoSchema } from '../models/create-task-dto.js'; -import { createTask } from '../services/task-service.js'; -import { badRequest, created, internalServerError } from '../utils/apigateway-response.js'; -import { logger } from '../utils/logger.js'; +import { CreateTaskDtoSchema } from '@/models/create-task-dto.js'; +import { createTask } from '@/services/task-service.js'; +import { badRequest, created, internalServerError } from '@/utils/apigateway-response.js'; +import { logger } from '@/utils/logger.js'; /** * Lambda request tracker middleware for logging. @@ -22,17 +22,12 @@ const withRequestTracking = lambdaRequestTracker(); */ export const handler = async (event: APIGatewayProxyEvent, context: Context): Promise => { withRequestTracking(event, context); - logger.info('[CreateTask] > handler', { - requestId: event.requestContext.requestId, - event, - }); + logger.info({ event, context }, '[CreateTaskHandler] > handler'); try { // Parse and validate request body if (!event.body) { - logger.warn('[CreateTask] < handler - missing request body', { - requestId: event.requestContext.requestId, - }); + logger.warn('[CreateTaskHandler] < handler - missing request body'); return badRequest('Request body is required'); } @@ -40,9 +35,7 @@ export const handler = async (event: APIGatewayProxyEvent, context: Context): Pr try { requestBody = JSON.parse(event.body); } catch (_error) { - logger.warn('[CreateTask] < handler - invalid JSON in request body', { - requestId: event.requestContext.requestId, - }); + logger.warn('[CreateTaskHandler] < handler - invalid JSON in request body'); return badRequest('Invalid JSON in request body'); } @@ -52,26 +45,29 @@ export const handler = async (event: APIGatewayProxyEvent, context: Context): Pr // Create the task const task = await createTask(validatedDto); - logger.info('[CreateTask] < handler - successfully created task', { - id: task.id, - requestId: event.requestContext.requestId, - }); - + logger.info( + { + id: task.id, + }, + '[CreateTaskHandler] < handler - successfully created task', + ); + // Return created response with the new task return created(task); } catch (error) { if (error instanceof ZodError) { + // Handle validation errors const errorMessages = error.issues.map((err) => `${err.path.join('.')}: ${err.message}`).join(', '); - logger.warn('[CreateTask] < handler - validation error', { - errors: error.issues, - requestId: event.requestContext.requestId, - }); + logger.warn( + { + errors: error.issues, + }, + '[CreateTaskHandler] < handler - validation error', + ); return badRequest(`Validation failed: ${errorMessages}`); } - logger.error('[CreateTask] < handler - failed to create task', error as Error, { - requestId: event.requestContext.requestId, - }); - + // Handle other errors + logger.error({ error }, '[CreateTaskHandler] < handler - failed to create task'); return internalServerError('Failed to create task'); } }; diff --git a/src/handlers/delete-task.test.ts b/src/handlers/delete-task.test.ts index cdd7f5b..2977c73 100644 --- a/src/handlers/delete-task.test.ts +++ b/src/handlers/delete-task.test.ts @@ -25,6 +25,7 @@ jest.mock('../utils/logger', () => ({ info: mockLoggerInfo, warn: mockLoggerWarn, error: mockLoggerError, + debug: jest.fn(), }, })); @@ -119,10 +120,16 @@ describe('delete-task handler', () => { expect(JSON.parse(result.body)).toEqual({}); expect(mockDeleteTask).toHaveBeenCalledTimes(1); expect(mockDeleteTask).toHaveBeenCalledWith('123e4567-e89b-12d3-a456-426614174000'); - expect(mockLoggerInfo).toHaveBeenCalledWith('[DeleteTask] > handler', expect.any(Object)); expect(mockLoggerInfo).toHaveBeenCalledWith( - '[DeleteTask] < handler - successfully deleted task', + expect.objectContaining({ + event: expect.any(Object), + context: expect.any(Object), + }), + '[DeleteTaskHandler] > handler', + ); + expect(mockLoggerInfo).toHaveBeenCalledWith( expect.any(Object), + '[DeleteTaskHandler] < handler - successfully deleted task', ); }); @@ -142,7 +149,7 @@ describe('delete-task handler', () => { }); expect(mockDeleteTask).toHaveBeenCalledTimes(1); expect(mockDeleteTask).toHaveBeenCalledWith('123e4567-e89b-12d3-a456-426614174000'); - expect(mockLoggerInfo).toHaveBeenCalledWith('[DeleteTask] < handler - task not found', expect.any(Object)); + expect(mockLoggerInfo).toHaveBeenCalledWith(expect.any(Object), '[DeleteTaskHandler] < handler - task not found'); }); it('should return 404 when taskId path parameter is missing', async () => { @@ -161,10 +168,7 @@ describe('delete-task handler', () => { message: 'Task not found', }); expect(mockDeleteTask).not.toHaveBeenCalled(); - expect(mockLoggerWarn).toHaveBeenCalledWith( - '[DeleteTask] < handler - missing taskId path parameter', - expect.any(Object), - ); + expect(mockLoggerWarn).toHaveBeenCalledWith('[DeleteTaskHandler] < handler - missing taskId path parameter'); }); it('should return 404 when taskId is undefined', async () => { @@ -202,9 +206,8 @@ describe('delete-task handler', () => { }); expect(mockDeleteTask).toHaveBeenCalledTimes(1); expect(mockLoggerError).toHaveBeenCalledWith( - '[DeleteTask] < handler - failed to delete task', - mockError, - expect.any(Object), + expect.objectContaining({ error: mockError }), + '[DeleteTaskHandler] < handler - failed to delete task', ); }); @@ -234,11 +237,11 @@ describe('delete-task handler', () => { // Assert expect(mockLoggerInfo).toHaveBeenCalledWith( - '[DeleteTask] > handler', expect.objectContaining({ - requestId: 'test-request-id', event: expect.any(Object), + context: expect.any(Object), }), + '[DeleteTaskHandler] > handler', ); }); @@ -286,11 +289,10 @@ describe('delete-task handler', () => { // Assert expect(mockLoggerInfo).toHaveBeenCalledWith( - '[DeleteTask] < handler - successfully deleted task', expect.objectContaining({ taskId, - requestId: 'test-request-id', }), + '[DeleteTaskHandler] < handler - successfully deleted task', ); }); @@ -306,11 +308,10 @@ describe('delete-task handler', () => { // Assert expect(mockLoggerInfo).toHaveBeenCalledWith( - '[DeleteTask] < handler - task not found', expect.objectContaining({ taskId, - requestId: 'test-request-id', }), + '[DeleteTaskHandler] < handler - task not found', ); }); }); diff --git a/src/handlers/delete-task.ts b/src/handlers/delete-task.ts index e70dc03..37f005f 100644 --- a/src/handlers/delete-task.ts +++ b/src/handlers/delete-task.ts @@ -1,9 +1,9 @@ import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; import { lambdaRequestTracker } from 'pino-lambda'; -import { deleteTask } from '../services/task-service.js'; -import { internalServerError, noContent, notFound } from '../utils/apigateway-response.js'; -import { logger } from '../utils/logger.js'; +import { deleteTask } from '@/services/task-service.js'; +import { internalServerError, noContent, notFound } from '@/utils/apigateway-response.js'; +import { logger } from '@/utils/logger.js'; /** * Lambda request tracker middleware for logging. @@ -20,42 +20,32 @@ const withRequestTracking = lambdaRequestTracker(); */ export const handler = async (event: APIGatewayProxyEvent, context: Context): Promise => { withRequestTracking(event, context); - logger.info('[DeleteTask] > handler', { - requestId: event.requestContext.requestId, - event, - }); + logger.info({ event, context }, '[DeleteTaskHandler] > handler'); try { + // Parse and validate the taskId from path parameters const taskId = event.pathParameters?.taskId; if (!taskId) { - logger.warn('[DeleteTask] < handler - missing taskId path parameter', { - requestId: event.requestContext.requestId, - }); + logger.warn('[DeleteTaskHandler] < handler - missing taskId path parameter'); return notFound('Task not found'); } + // Delete the task const deleted = await deleteTask(taskId); + // Check if the task was found and deleted if (!deleted) { - logger.info('[DeleteTask] < handler - task not found', { - taskId, - requestId: event.requestContext.requestId, - }); + logger.info({ taskId }, '[DeleteTaskHandler] < handler - task not found'); return notFound('Task not found'); } - logger.info('[DeleteTask] < handler - successfully deleted task', { - taskId, - requestId: event.requestContext.requestId, - }); - + // Return no content response + logger.info({ taskId }, '[DeleteTaskHandler] < handler - successfully deleted task'); return noContent(); } catch (error) { - logger.error('[DeleteTask] < handler - failed to delete task', error as Error, { - requestId: event.requestContext.requestId, - }); - + // Handle unexpected errors + logger.error({ error }, '[DeleteTaskHandler] < handler - failed to delete task'); return internalServerError('Failed to delete task'); } }; diff --git a/src/handlers/get-task.test.ts b/src/handlers/get-task.test.ts index f36eb52..5b951b7 100644 --- a/src/handlers/get-task.test.ts +++ b/src/handlers/get-task.test.ts @@ -27,6 +27,7 @@ jest.mock('../utils/logger', () => ({ info: mockLoggerInfo, warn: mockLoggerWarn, error: mockLoggerError, + debug: jest.fn(), }, })); @@ -130,10 +131,16 @@ describe('get-task handler', () => { expect(JSON.parse(result.body)).toEqual(mockTask); expect(mockGetTask).toHaveBeenCalledTimes(1); expect(mockGetTask).toHaveBeenCalledWith('123e4567-e89b-12d3-a456-426614174000'); - expect(mockLoggerInfo).toHaveBeenCalledWith('[GetTask] > handler', expect.any(Object)); expect(mockLoggerInfo).toHaveBeenCalledWith( - '[GetTask] < handler - successfully retrieved task', + expect.objectContaining({ + event: expect.any(Object), + context: expect.any(Object), + }), + '[GetTaskHandler] > handler', + ); + expect(mockLoggerInfo).toHaveBeenCalledWith( expect.any(Object), + '[GetTaskHandler] < handler - successfully retrieved task', ); }); @@ -153,7 +160,7 @@ describe('get-task handler', () => { }); expect(mockGetTask).toHaveBeenCalledTimes(1); expect(mockGetTask).toHaveBeenCalledWith('123e4567-e89b-12d3-a456-426614174000'); - expect(mockLoggerInfo).toHaveBeenCalledWith('[GetTask] < handler - task not found', expect.any(Object)); + expect(mockLoggerInfo).toHaveBeenCalledWith(expect.any(Object), '[GetTaskHandler] < handler - task not found'); }); it('should return 404 when taskId path parameter is missing', async () => { @@ -172,10 +179,7 @@ describe('get-task handler', () => { message: 'Task not found', }); expect(mockGetTask).not.toHaveBeenCalled(); - expect(mockLoggerWarn).toHaveBeenCalledWith( - '[GetTask] < handler - missing taskId path parameter', - expect.any(Object), - ); + expect(mockLoggerWarn).toHaveBeenCalledWith('[GetTaskHandler] < handler - missing taskId path parameter'); }); it('should return 404 when taskId is undefined', async () => { @@ -213,9 +217,8 @@ describe('get-task handler', () => { }); expect(mockGetTask).toHaveBeenCalledTimes(1); expect(mockLoggerError).toHaveBeenCalledWith( - '[GetTask] < handler - failed to get task', - mockError, - expect.any(Object), + expect.objectContaining({ error: mockError }), + '[GetTaskHandler] < handler - failed to get task', ); }); @@ -261,11 +264,11 @@ describe('get-task handler', () => { // Assert expect(mockLoggerInfo).toHaveBeenCalledWith( - '[GetTask] > handler', expect.objectContaining({ - requestId: 'test-request-id', event: expect.any(Object), + context: expect.any(Object), }), + '[GetTaskHandler] > handler', ); }); diff --git a/src/handlers/get-task.ts b/src/handlers/get-task.ts index f97ca1c..cb88292 100644 --- a/src/handlers/get-task.ts +++ b/src/handlers/get-task.ts @@ -1,9 +1,9 @@ import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; import { lambdaRequestTracker } from 'pino-lambda'; -import { getTask } from '../services/task-service.js'; -import { internalServerError, notFound, ok } from '../utils/apigateway-response.js'; -import { logger } from '../utils/logger.js'; +import { getTask } from '@/services/task-service.js'; +import { internalServerError, notFound, ok } from '@/utils/apigateway-response.js'; +import { logger } from '@/utils/logger.js'; /** * Lambda request tracker middleware for logging. @@ -20,42 +20,32 @@ const withRequestTracking = lambdaRequestTracker(); */ export const handler = async (event: APIGatewayProxyEvent, context: Context): Promise => { withRequestTracking(event, context); - logger.info('[GetTask] > handler', { - requestId: event.requestContext.requestId, - event, - }); + logger.info({ event, context }, '[GetTaskHandler] > handler'); try { + // Parse and validate the taskId from path parameters const taskId = event.pathParameters?.taskId; if (!taskId) { - logger.warn('[GetTask] < handler - missing taskId path parameter', { - requestId: event.requestContext.requestId, - }); + logger.warn('[GetTaskHandler] < handler - missing taskId path parameter'); return notFound('Task not found'); } + // Retrieve the task const task = await getTask(taskId); + // Check if the task was found if (!task) { - logger.info('[GetTask] < handler - task not found', { - taskId, - requestId: event.requestContext.requestId, - }); + logger.info({ taskId }, '[GetTaskHandler] < handler - task not found'); return notFound('Task not found'); } - logger.info('[GetTask] < handler - successfully retrieved task', { - taskId, - requestId: event.requestContext.requestId, - }); - + // Return ok response with the task + logger.info({ taskId }, '[GetTaskHandler] < handler - successfully retrieved task'); return ok(task); } catch (error) { - logger.error('[GetTask] < handler - failed to get task', error as Error, { - requestId: event.requestContext.requestId, - }); - + // Handle unexpected errors + logger.error({ error }, '[GetTaskHandler] < handler - failed to get task'); return internalServerError('Failed to retrieve task'); } }; diff --git a/src/handlers/list-tasks.test.ts b/src/handlers/list-tasks.test.ts index c458bfb..e2959b7 100644 --- a/src/handlers/list-tasks.test.ts +++ b/src/handlers/list-tasks.test.ts @@ -25,6 +25,8 @@ jest.mock('../utils/logger', () => ({ logger: { info: mockLoggerInfo, error: mockLoggerError, + debug: jest.fn(), + warn: jest.fn(), }, })); @@ -134,10 +136,16 @@ describe('list-tasks handler', () => { expect(result.statusCode).toBe(200); expect(JSON.parse(result.body)).toEqual(mockTasks); expect(mockListTasks).toHaveBeenCalledTimes(1); - expect(mockLoggerInfo).toHaveBeenCalledWith('[ListTasks] > handler', expect.any(Object)); expect(mockLoggerInfo).toHaveBeenCalledWith( - '[ListTasks] < handler - successfully retrieved tasks', + expect.objectContaining({ + event: expect.any(Object), + context: expect.any(Object), + }), + '[ListTasksHandler] > handler', + ); + expect(mockLoggerInfo).toHaveBeenCalledWith( expect.any(Object), + '[ListTasksHandler] < handler - successfully retrieved tasks', ); }); @@ -173,9 +181,8 @@ describe('list-tasks handler', () => { }); expect(mockListTasks).toHaveBeenCalledTimes(1); expect(mockLoggerError).toHaveBeenCalledWith( - '[ListTasks] < handler - failed to list tasks', - mockError, - expect.any(Object), + expect.objectContaining({ error: mockError }), + '[ListTasksHandler] < handler - failed to list tasks', ); }); @@ -204,10 +211,10 @@ describe('list-tasks handler', () => { await handler(event, context); // Assert - expect(mockLoggerInfo).toHaveBeenCalledWith('[ListTasks] > handler', { - requestId: 'test-request-id', - event, - }); + expect(mockLoggerInfo).toHaveBeenCalledWith( + { context: expect.any(Object), event: expect.any(Object) }, + '[ListTasksHandler] > handler', + ); }); it('should log successful response with count', async () => { @@ -229,10 +236,12 @@ describe('list-tasks handler', () => { await handler(event, context); // Assert - expect(mockLoggerInfo).toHaveBeenCalledWith('[ListTasks] < handler - successfully retrieved tasks', { - count: 1, - requestId: 'test-request-id', - }); + expect(mockLoggerInfo).toHaveBeenCalledWith( + { + count: 1, + }, + '[ListTasksHandler] < handler - successfully retrieved tasks', + ); }); }); }); diff --git a/src/handlers/list-tasks.ts b/src/handlers/list-tasks.ts index 873a3d2..81a7463 100644 --- a/src/handlers/list-tasks.ts +++ b/src/handlers/list-tasks.ts @@ -1,9 +1,9 @@ import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; import { lambdaRequestTracker } from 'pino-lambda'; -import { listTasks } from '../services/task-service.js'; -import { internalServerError, ok } from '../utils/apigateway-response.js'; -import { logger } from '../utils/logger.js'; +import { listTasks } from '@/services/task-service.js'; +import { internalServerError, ok } from '@/utils/apigateway-response.js'; +import { logger } from '@/utils/logger.js'; /** * Lambda request tracker middleware for logging. @@ -20,25 +20,18 @@ const withRequestTracking = lambdaRequestTracker(); */ export const handler = async (event: APIGatewayProxyEvent, context: Context): Promise => { withRequestTracking(event, context); - logger.info('[ListTasks] > handler', { - requestId: event.requestContext.requestId, - event, - }); + logger.info({ event, context }, '[ListTasksHandler] > handler'); try { + // Retrieve the list of tasks const tasks = await listTasks(); - logger.info('[ListTasks] < handler - successfully retrieved tasks', { - count: tasks.length, - requestId: event.requestContext.requestId, - }); - + // Return ok response with the list of tasks + logger.info({ count: tasks.length }, '[ListTasksHandler] < handler - successfully retrieved tasks'); return ok(tasks); } catch (error) { - logger.error('[ListTasks] < handler - failed to list tasks', error as Error, { - requestId: event.requestContext.requestId, - }); - + // Handle unexpected errors + logger.error({ error }, '[ListTasksHandler] < handler - failed to list tasks'); return internalServerError('Failed to retrieve tasks'); } }; diff --git a/src/handlers/update-task.test.ts b/src/handlers/update-task.test.ts index ef9cfb5..a1ffe92 100644 --- a/src/handlers/update-task.test.ts +++ b/src/handlers/update-task.test.ts @@ -27,6 +27,7 @@ jest.mock('../utils/logger', () => ({ info: mockLoggerInfo, warn: mockLoggerWarn, error: mockLoggerError, + debug: jest.fn(), }, })); @@ -137,10 +138,16 @@ describe('update-task handler', () => { expect(JSON.parse(result.body)).toEqual(mockTask); expect(mockUpdateTask).toHaveBeenCalledTimes(1); expect(mockUpdateTask).toHaveBeenCalledWith(taskId, requestBody); - expect(mockLoggerInfo).toHaveBeenCalledWith('[UpdateTask] > handler', expect.any(Object)); expect(mockLoggerInfo).toHaveBeenCalledWith( - '[UpdateTask] < handler - successfully updated task', + expect.objectContaining({ + event: expect.any(Object), + context: expect.any(Object), + }), + '[UpdateTaskHandler] > handler', + ); + expect(mockLoggerInfo).toHaveBeenCalledWith( expect.any(Object), + '[UpdateTaskHandler] < handler - successfully updated task', ); }); @@ -193,7 +200,7 @@ describe('update-task handler', () => { expect(result.statusCode).toBe(400); expect(JSON.parse(result.body)).toEqual({ message: 'Task ID is required' }); expect(mockUpdateTask).not.toHaveBeenCalled(); - expect(mockLoggerWarn).toHaveBeenCalledWith('[UpdateTask] < handler - missing taskId', expect.any(Object)); + expect(mockLoggerWarn).toHaveBeenCalledWith('[UpdateTaskHandler] < handler - missing taskId'); }); it('should return 400 when request body is missing', async () => { @@ -208,7 +215,7 @@ describe('update-task handler', () => { expect(result.statusCode).toBe(400); expect(JSON.parse(result.body)).toEqual({ message: 'Request body is required' }); expect(mockUpdateTask).not.toHaveBeenCalled(); - expect(mockLoggerWarn).toHaveBeenCalledWith('[UpdateTask] < handler - missing request body', expect.any(Object)); + expect(mockLoggerWarn).toHaveBeenCalledWith('[UpdateTaskHandler] < handler - missing request body'); }); it('should return 400 when request body is not valid JSON', async () => { @@ -223,10 +230,7 @@ describe('update-task handler', () => { expect(result.statusCode).toBe(400); expect(JSON.parse(result.body)).toEqual({ message: 'Invalid JSON in request body' }); expect(mockUpdateTask).not.toHaveBeenCalled(); - expect(mockLoggerWarn).toHaveBeenCalledWith( - '[UpdateTask] < handler - invalid JSON in request body', - expect.any(Object), - ); + expect(mockLoggerWarn).toHaveBeenCalledWith('[UpdateTaskHandler] < handler - invalid JSON in request body'); }); it('should return 404 when task is not found', async () => { @@ -246,7 +250,7 @@ describe('update-task handler', () => { // Assert expect(result.statusCode).toBe(404); expect(mockUpdateTask).toHaveBeenCalledTimes(1); - expect(mockLoggerInfo).toHaveBeenCalledWith('[UpdateTask] < handler - task not found', expect.any(Object)); + expect(mockLoggerInfo).toHaveBeenCalledWith(expect.any(Object), '[UpdateTaskHandler] < handler - task not found'); }); it('should return 400 when title is missing', async () => { @@ -268,7 +272,10 @@ describe('update-task handler', () => { expect(responseBody.message).toContain('Validation failed'); expect(responseBody.message).toContain('title'); expect(mockUpdateTask).not.toHaveBeenCalled(); - expect(mockLoggerWarn).toHaveBeenCalledWith('[UpdateTask] < handler - validation error', expect.any(Object)); + expect(mockLoggerWarn).toHaveBeenCalledWith( + expect.any(Object), + '[UpdateTaskHandler] < handler - validation error', + ); }); it('should return 400 when title is empty', async () => { @@ -454,9 +461,8 @@ describe('update-task handler', () => { expect(JSON.parse(result.body)).toEqual({ message: 'Failed to update task' }); expect(mockUpdateTask).toHaveBeenCalledTimes(1); expect(mockLoggerError).toHaveBeenCalledWith( - '[UpdateTask] < handler - failed to update task', - mockError, - expect.any(Object), + expect.objectContaining({ error: mockError }), + '[UpdateTaskHandler] < handler - failed to update task', ); }); diff --git a/src/handlers/update-task.ts b/src/handlers/update-task.ts index 539cfc0..f4f33f4 100644 --- a/src/handlers/update-task.ts +++ b/src/handlers/update-task.ts @@ -2,10 +2,10 @@ import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda import { lambdaRequestTracker } from 'pino-lambda'; import { ZodError } from 'zod'; -import { UpdateTaskDtoSchema } from '../models/update-task-dto.js'; -import { updateTask } from '../services/task-service.js'; -import { badRequest, internalServerError, notFound, ok } from '../utils/apigateway-response.js'; -import { logger } from '../utils/logger.js'; +import { UpdateTaskDtoSchema } from '@/models/update-task-dto.js'; +import { updateTask } from '@/services/task-service.js'; +import { badRequest, internalServerError, notFound, ok } from '@/utils/apigateway-response.js'; +import { logger } from '@/utils/logger.js'; /** * Lambda request tracker middleware for logging. @@ -22,26 +22,19 @@ const withRequestTracking = lambdaRequestTracker(); */ export const handler = async (event: APIGatewayProxyEvent, context: Context): Promise => { withRequestTracking(event, context); - logger.info('[UpdateTask] > handler', { - requestId: event.requestContext.requestId, - event, - }); + logger.info({ event, context }, '[UpdateTaskHandler] > handler'); try { - // Extract taskId from path parameters + // Parse and validate the taskId from path parameters const taskId = event.pathParameters?.taskId; if (!taskId) { - logger.warn('[UpdateTask] < handler - missing taskId', { - requestId: event.requestContext.requestId, - }); + logger.warn('[UpdateTaskHandler] < handler - missing taskId'); return badRequest('Task ID is required'); } // Parse and validate request body if (!event.body) { - logger.warn('[UpdateTask] < handler - missing request body', { - requestId: event.requestContext.requestId, - }); + logger.warn('[UpdateTaskHandler] < handler - missing request body'); return badRequest('Request body is required'); } @@ -49,9 +42,7 @@ export const handler = async (event: APIGatewayProxyEvent, context: Context): Pr try { requestBody = JSON.parse(event.body); } catch (_error) { - logger.warn('[UpdateTask] < handler - invalid JSON in request body', { - requestId: event.requestContext.requestId, - }); + logger.warn('[UpdateTaskHandler] < handler - invalid JSON in request body'); return badRequest('Invalid JSON in request body'); } @@ -61,34 +52,25 @@ export const handler = async (event: APIGatewayProxyEvent, context: Context): Pr // Update the task const task = await updateTask(taskId, validatedDto); + // Check if the task was found if (!task) { - logger.info('[UpdateTask] < handler - task not found', { - taskId, - requestId: event.requestContext.requestId, - }); + logger.info({ taskId }, '[UpdateTaskHandler] < handler - task not found'); return notFound(); } - logger.info('[UpdateTask] < handler - successfully updated task', { - id: task.id, - requestId: event.requestContext.requestId, - }); - + // Return ok response with the updated task + logger.info({ id: task.id }, '[UpdateTaskHandler] < handler - successfully updated task'); return ok(task); } catch (error) { + // Handle validation errors if (error instanceof ZodError) { const errorMessages = error.issues.map((err) => `${err.path.join('.')}: ${err.message}`).join(', '); - logger.warn('[UpdateTask] < handler - validation error', { - errors: error.issues, - requestId: event.requestContext.requestId, - }); + logger.warn({ issues: error.issues }, '[UpdateTaskHandler] < handler - validation error'); return badRequest(`Validation failed: ${errorMessages}`); } - logger.error('[UpdateTask] < handler - failed to update task', error as Error, { - requestId: event.requestContext.requestId, - }); - + // Handle other unexpected errors + logger.error({ error }, '[UpdateTaskHandler] < handler - failed to update task'); return internalServerError('Failed to update task'); } }; diff --git a/src/services/task-service.test.ts b/src/services/task-service.test.ts index d378ea4..95782e3 100644 --- a/src/services/task-service.test.ts +++ b/src/services/task-service.test.ts @@ -67,9 +67,7 @@ describe('task-service', () => { // Assert expect(result).toEqual([]); expect(mockSend).toHaveBeenCalledTimes(1); - expect(mockLoggerInfo).toHaveBeenCalledWith('[TaskService] > listTasks', { - tableName: 'test-tasks-table', - }); + expect(mockLoggerInfo).toHaveBeenCalledWith('[TaskService] > listTasks'); }); it('should return all tasks when they exist', async () => { @@ -180,10 +178,12 @@ describe('task-service', () => { }), }), ); - expect(mockLoggerInfo).toHaveBeenCalledWith('[TaskService] > getTask', { - tableName: 'test-tasks-table', - id: '123e4567-e89b-12d3-a456-426614174000', - }); + expect(mockLoggerInfo).toHaveBeenCalledWith( + { + id: '123e4567-e89b-12d3-a456-426614174000', + }, + '[TaskService] > getTask', + ); }); it('should return null when task does not exist', async () => { @@ -196,9 +196,12 @@ describe('task-service', () => { // Assert expect(result).toBeNull(); expect(mockSend).toHaveBeenCalledTimes(1); - expect(mockLoggerInfo).toHaveBeenCalledWith('[TaskService] < getTask - task not found', { - id: 'non-existent-id', - }); + expect(mockLoggerInfo).toHaveBeenCalledWith( + { + id: 'non-existent-id', + }, + '[TaskService] < getTask - task not found', + ); }); it('should return a task with only required fields', async () => { @@ -319,9 +322,7 @@ describe('task-service', () => { }), }), ); - expect(mockLoggerInfo).toHaveBeenCalledWith('[TaskService] > createTask', { - tableName: 'test-tasks-table', - }); + expect(mockLoggerInfo).toHaveBeenCalledWith('[TaskService] > createTask'); }); it('should create a task with only required fields', async () => { @@ -500,10 +501,12 @@ describe('task-service', () => { expect(result?.isComplete).toBe(true); expect(result?.updatedAt).toBe('2025-12-01T10:00:00.000Z'); expect(mockSend).toHaveBeenCalledTimes(1); - expect(mockLoggerInfo).toHaveBeenCalledWith('[TaskService] > updateTask', { - tableName: 'test-tasks-table', - id: taskId, - }); + expect(mockLoggerInfo).toHaveBeenCalledWith( + { + id: taskId, + }, + '[TaskService] > updateTask', + ); }); it('should update a task with only required fields', async () => { @@ -557,9 +560,12 @@ describe('task-service', () => { // Assert expect(result).toBeNull(); expect(mockSend).toHaveBeenCalledTimes(1); - expect(mockLoggerInfo).toHaveBeenCalledWith('[TaskService] < updateTask - task not found', { - id: taskId, - }); + expect(mockLoggerInfo).toHaveBeenCalledWith( + { + id: taskId, + }, + '[TaskService] < updateTask - task not found', + ); }); it('should update task and set updatedAt to current time', async () => { @@ -752,13 +758,18 @@ describe('task-service', () => { }), }), ); - expect(mockLoggerInfo).toHaveBeenCalledWith('[TaskService] > deleteTask', { - tableName: 'test-tasks-table', - id: taskId, - }); - expect(mockLoggerInfo).toHaveBeenCalledWith('[TaskService] < deleteTask - successfully deleted task', { - id: taskId, - }); + expect(mockLoggerInfo).toHaveBeenCalledWith( + { + id: taskId, + }, + '[TaskService] > deleteTask', + ); + expect(mockLoggerInfo).toHaveBeenCalledWith( + { + id: taskId, + }, + '[TaskService] < deleteTask - successfully deleted task', + ); }); it('should return false when task does not exist', async () => { @@ -774,9 +785,12 @@ describe('task-service', () => { // Assert expect(result).toBe(false); expect(mockSend).toHaveBeenCalledTimes(1); - expect(mockLoggerInfo).toHaveBeenCalledWith('[TaskService] < deleteTask - task not found', { - id: taskId, - }); + expect(mockLoggerInfo).toHaveBeenCalledWith( + { + id: taskId, + }, + '[TaskService] < deleteTask - task not found', + ); }); it('should handle DynamoDB errors and rethrow them', async () => { @@ -789,12 +803,8 @@ describe('task-service', () => { await expect(deleteTask(taskId)).rejects.toThrow('DynamoDB error'); expect(mockSend).toHaveBeenCalledTimes(1); expect(mockLoggerError).toHaveBeenCalledWith( + expect.objectContaining({ error: mockError }), '[TaskService] < deleteTask - failed to delete task from DynamoDB', - mockError, - { - tableName: 'test-tasks-table', - id: taskId, - }, ); }); diff --git a/src/services/task-service.ts b/src/services/task-service.ts index 60e0899..2b91aab 100644 --- a/src/services/task-service.ts +++ b/src/services/task-service.ts @@ -1,12 +1,12 @@ import { randomUUID } from 'crypto'; import { DeleteCommand, GetCommand, PutCommand, ScanCommand, UpdateCommand } from '@aws-sdk/lib-dynamodb'; -import { CreateTaskDto } from '../models/create-task-dto.js'; -import { UpdateTaskDto } from '../models/update-task-dto.js'; -import { Task, TaskItem, TaskKeys, toTask } from '../models/task.js'; -import { config } from '../utils/config.js'; -import { dynamoDocClient } from '../utils/dynamodb-client.js'; -import { logger } from '../utils/logger.js'; +import { CreateTaskDto } from '@/models/create-task-dto.js'; +import { UpdateTaskDto } from '@/models/update-task-dto.js'; +import { Task, TaskItem, TaskKeys, toTask } from '@/models/task.js'; +import { config } from '@/utils/config.js'; +import { dynamoDocClient } from '@/utils/dynamodb-client.js'; +import { logger } from '@/utils/logger.js'; /** * Retrieves all tasks from the DynamoDB table @@ -14,29 +14,34 @@ import { logger } from '../utils/logger.js'; * @throws Error if the DynamoDB scan operation fails */ export const listTasks = async (): Promise => { - logger.info('[TaskService] > listTasks', { tableName: config.TASKS_TABLE }); + logger.info('[TaskService] > listTasks'); try { + // Use ScanCommand to retrieve all items from the tasks table const command = new ScanCommand({ TableName: config.TASKS_TABLE, }); - logger.debug('[TaskService] listTasks - ScanCommand', { command }); + logger.debug({ input: command.input }, '[TaskService] listTasks - ScanCommandInput'); + // Execute the scan command const response = await dynamoDocClient.send(command); + // Map the retrieved items to Task objects const taskItems = (response.Items as TaskItem[]) ?? []; const tasks = taskItems.map(toTask); - logger.info('[TaskService] < listTasks - successfully retrieved tasks', { - count: tasks.length, - scannedCount: response.ScannedCount, - }); + logger.info( + { + count: tasks.length, + scannedCount: response.ScannedCount, + }, + '[TaskService] < listTasks - successfully retrieved tasks', + ); return tasks; } catch (error) { - logger.error('[TaskService] < listTasks - failed to fetch tasks from DynamoDB', error as Error, { - tableName: config.TASKS_TABLE, - }); + // Handle unexpected errors + logger.error({ error }, '[TaskService] < listTasks - failed to fetch tasks from DynamoDB'); throw error; } }; @@ -48,34 +53,36 @@ export const listTasks = async (): Promise => { * @throws Error if the DynamoDB get operation fails */ export const getTask = async (id: string): Promise => { - logger.info('[TaskService] > getTask', { tableName: config.TASKS_TABLE, id }); + logger.info({ id }, '[TaskService] > getTask'); try { + // Use GetCommand to retrieve the task by its primary key const command = new GetCommand({ TableName: config.TASKS_TABLE, Key: { pk: TaskKeys.pk(id), }, }); - logger.debug('[TaskService] getTask - GetCommand', { command }); + logger.debug({ input: command.input }, '[TaskService] getTask - GetCommandInput'); + // Execute the get command const response = await dynamoDocClient.send(command); + // Check if the task was found if (!response.Item) { - logger.info('[TaskService] < getTask - task not found', { id }); + logger.info({ id }, '[TaskService] < getTask - task not found'); return null; } + // Map the retrieved item to a Task object const task = toTask(response.Item as TaskItem); - logger.info('[TaskService] < getTask - successfully retrieved task', { id }); + logger.info({ id }, '[TaskService] < getTask - successfully retrieved task'); return task; } catch (error) { - logger.error('[TaskService] < getTask - failed to fetch task from DynamoDB', error as Error, { - tableName: config.TASKS_TABLE, - id, - }); + // Handle unexpected errors + logger.error({ id, error }, '[TaskService] < getTask - failed to fetch task from DynamoDB'); throw error; } }; @@ -87,9 +94,10 @@ export const getTask = async (id: string): Promise => { * @throws Error if the DynamoDB put operation fails */ export const createTask = async (createTaskDto: CreateTaskDto): Promise => { - logger.info('[TaskService] > createTask', { tableName: config.TASKS_TABLE }); + logger.info('[TaskService] > createTask'); try { + // Prepare the task item to be inserted const id = randomUUID(); const now = new Date().toISOString(); @@ -108,21 +116,20 @@ export const createTask = async (createTaskDto: CreateTaskDto): Promise => TableName: config.TASKS_TABLE, Item: taskItem, }); - logger.debug('[TaskService] createTask - PutCommand', { command }); + logger.debug({ input: command.input }, '[TaskService] createTask - PutCommandInput'); + // Execute the put command to create the task await dynamoDocClient.send(command); + // Map the created item to a Task object const task = toTask(taskItem); - logger.info('[TaskService] < createTask - successfully created task', { - id: task.id, - }); + logger.info({ id: task.id }, '[TaskService] < createTask - successfully created task'); return task; } catch (error) { - logger.error('[TaskService] < createTask - failed to create task in DynamoDB', error as Error, { - tableName: config.TASKS_TABLE, - }); + // Handle unexpected errors + logger.error({ error }, '[TaskService] < createTask - failed to create task in DynamoDB'); throw error; } }; @@ -135,9 +142,10 @@ export const createTask = async (createTaskDto: CreateTaskDto): Promise => * @throws Error if the DynamoDB update operation fails */ export const updateTask = async (id: string, updateTaskDto: UpdateTaskDto): Promise => { - logger.info('[TaskService] > updateTask', { tableName: config.TASKS_TABLE, id }); + logger.info({ id }, '[TaskService] > updateTask'); try { + // Prepare the task item to be updated const now = new Date().toISOString(); // Build update expression dynamically @@ -187,31 +195,32 @@ export const updateTask = async (id: string, updateTaskDto: UpdateTaskDto): Prom ConditionExpression: 'attribute_exists(pk)', ReturnValues: 'ALL_NEW', }); - logger.debug('[TaskService] updateTask - UpdateCommand', { command }); + logger.debug({ input: command.input }, '[TaskService] updateTask - UpdateCommandInput'); + // Execute the update command const response = await dynamoDocClient.send(command); + // Check if the task was found if (!response.Attributes) { - logger.info('[TaskService] < updateTask - task not found', { id }); + logger.info({ id }, '[TaskService] < updateTask - task not found'); return null; } + // Map the updated item to a Task object const task = toTask(response.Attributes as TaskItem); - logger.info('[TaskService] < updateTask - successfully updated task', { id }); + logger.info({ id }, '[TaskService] < updateTask - successfully updated task'); return task; } catch (error) { - // Check if the error is a conditional check failure (task not found) + // Handle conditional check failures (task not found) if (error instanceof Error && error.name === 'ConditionalCheckFailedException') { - logger.info('[TaskService] < updateTask - task not found', { id }); + logger.info({ id }, '[TaskService] < updateTask - task not found'); return null; } - logger.error('[TaskService] < updateTask - failed to update task in DynamoDB', error as Error, { - tableName: config.TASKS_TABLE, - id, - }); + // Handle unexpected errors + logger.error({ error }, '[TaskService] < updateTask - failed to update task in DynamoDB'); throw error; } }; @@ -223,9 +232,10 @@ export const updateTask = async (id: string, updateTaskDto: UpdateTaskDto): Prom * @throws Error if the DynamoDB delete operation fails */ export const deleteTask = async (id: string): Promise => { - logger.info('[TaskService] > deleteTask', { tableName: config.TASKS_TABLE, id }); + logger.info({ id }, '[TaskService] > deleteTask'); try { + // Use DeleteCommand to delete the task by its primary key const command = new DeleteCommand({ TableName: config.TASKS_TABLE, Key: { @@ -233,24 +243,22 @@ export const deleteTask = async (id: string): Promise => { }, ConditionExpression: 'attribute_exists(pk)', }); - logger.debug('[TaskService] deleteTask - DeleteCommand', { command }); + logger.debug({ input: command.input }, '[TaskService] deleteTask - DeleteCommandInput'); + // Execute the delete command await dynamoDocClient.send(command); - logger.info('[TaskService] < deleteTask - successfully deleted task', { id }); - + logger.info({ id }, '[TaskService] < deleteTask - successfully deleted task'); return true; } catch (error) { - // Check if the error is a conditional check failure (task not found) + // Handle conditional check failures (task not found) if (error instanceof Error && error.name === 'ConditionalCheckFailedException') { - logger.info('[TaskService] < deleteTask - task not found', { id }); + logger.info({ id }, '[TaskService] < deleteTask - task not found'); return false; } - logger.error('[TaskService] < deleteTask - failed to delete task from DynamoDB', error as Error, { - tableName: config.TASKS_TABLE, - id, - }); + // Handle unexpected errors + logger.error({ error }, '[TaskService] < deleteTask - failed to delete task from DynamoDB'); throw error; } }; diff --git a/src/utils/dynamodb-client.test.ts b/src/utils/dynamodb-client.test.ts index 16e7bee..b2e86c6 100644 --- a/src/utils/dynamodb-client.test.ts +++ b/src/utils/dynamodb-client.test.ts @@ -62,11 +62,14 @@ describe('dynamodb-client', () => { require('./dynamodb-client'); // Assert - expect(mockLoggerInfo).toHaveBeenCalledWith('[DynamoDBClient] - Initialized AWS DynamoDB client', { - config: { - region: 'us-east-1', - }, - }); + expect(mockLoggerInfo).toHaveBeenCalledWith( + expect.objectContaining({ + dynamoDBClientConfig: expect.objectContaining({ + region: 'us-east-1', + }), + }), + '[DynamoDBClient] - Initialized AWS DynamoDB client', + ); }); }); @@ -132,11 +135,14 @@ describe('dynamodb-client', () => { require('./dynamodb-client'); // Assert - expect(mockLoggerInfo).toHaveBeenCalledWith('[DynamoDBClient] - Initialized AWS DynamoDB client', { - config: { - region: 'eu-west-1', - }, - }); + expect(mockLoggerInfo).toHaveBeenCalledWith( + expect.objectContaining({ + dynamoDBClientConfig: expect.objectContaining({ + region: 'eu-west-1', + }), + }), + '[DynamoDBClient] - Initialized AWS DynamoDB client', + ); }); }); diff --git a/src/utils/dynamodb-client.ts b/src/utils/dynamodb-client.ts index 5016261..454084b 100644 --- a/src/utils/dynamodb-client.ts +++ b/src/utils/dynamodb-client.ts @@ -21,4 +21,4 @@ export const dynamoClient = new DynamoDBClient(clientConfig); */ export const dynamoDocClient = DynamoDBDocumentClient.from(dynamoClient); -logger.info('[DynamoDBClient] - Initialized AWS DynamoDB client', { config: clientConfig }); +logger.info({ dynamoDBClientConfig: clientConfig }, '[DynamoDBClient] - Initialized AWS DynamoDB client'); diff --git a/src/utils/lambda-client.test.ts b/src/utils/lambda-client.test.ts index 5563c4f..ae980b2 100644 --- a/src/utils/lambda-client.test.ts +++ b/src/utils/lambda-client.test.ts @@ -58,13 +58,16 @@ describe('lambda-client', () => { // Assert expect(result).toEqual(mockResponse); - expect(mockLoggerInfo).toHaveBeenCalledWith('[LambdaClient] > invokeLambdaSync', { functionName }); expect(mockLoggerInfo).toHaveBeenCalledWith( - '[LambdaClient] < invokeLambdaSync - successfully invoked Lambda function', - { + expect.objectContaining({ functionName }), + '[LambdaClient] > invokeLambdaSync', + ); + expect(mockLoggerInfo).toHaveBeenCalledWith( + expect.objectContaining({ functionName, statusCode: 200, - }, + }), + '[LambdaClient] < invokeLambdaSync - successfully invoked Lambda function', ); }); @@ -144,12 +147,12 @@ describe('lambda-client', () => { // Act & Assert await expect(invokeLambdaSync(functionName, payload)).rejects.toThrow('Lambda function error: Unhandled'); expect(mockLoggerError).toHaveBeenCalledWith( - '[LambdaClient] < invokeLambdaSync - Lambda function returned an error', - expect.any(Error), expect.objectContaining({ functionName, FunctionError: 'Unhandled', + responsePayload: expect.any(Object), }), + '[LambdaClient] < invokeLambdaSync - Lambda function returned an error', ); }); @@ -164,9 +167,11 @@ describe('lambda-client', () => { // Act & Assert await expect(invokeLambdaSync(functionName, payload)).rejects.toThrow('Function not found'); expect(mockLoggerError).toHaveBeenCalledWith( + expect.objectContaining({ + error, + functionName, + }), '[LambdaClient] < invokeLambdaSync - failed to invoke Lambda function', - error, - { functionName }, ); }); @@ -213,8 +218,8 @@ describe('lambda-client', () => { // Assert expect(mockLoggerDebug).toHaveBeenCalledWith( - '[LambdaClient] invokeLambdaSync - InvokeCommand', expect.any(Object), + '[LambdaClient] invokeLambdaSync - InvokeCommand', ); }); @@ -324,17 +329,20 @@ describe('lambda-client', () => { await invokeLambdaSync(functionName, payload); // Assert - expect(mockLoggerInfo).toHaveBeenCalledWith('[LambdaClient] > invokeLambdaSync', { functionName }); + expect(mockLoggerInfo).toHaveBeenCalledWith( + expect.objectContaining({ functionName }), + '[LambdaClient] > invokeLambdaSync', + ); expect(mockLoggerDebug).toHaveBeenCalledWith( - '[LambdaClient] invokeLambdaSync - InvokeCommand', expect.any(Object), + '[LambdaClient] invokeLambdaSync - InvokeCommand', ); expect(mockLoggerInfo).toHaveBeenCalledWith( - '[LambdaClient] < invokeLambdaSync - successfully invoked Lambda function', - { + expect.objectContaining({ functionName, statusCode: 200, - }, + }), + '[LambdaClient] < invokeLambdaSync - successfully invoked Lambda function', ); }); }); @@ -355,13 +363,16 @@ describe('lambda-client', () => { await invokeLambdaAsync(functionName, payload); // Assert - expect(mockLoggerInfo).toHaveBeenCalledWith('[LambdaClient] > invokeLambdaAsync', { functionName }); expect(mockLoggerInfo).toHaveBeenCalledWith( - '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', - { + expect.objectContaining({ functionName }), + '[LambdaClient] > invokeLambdaAsync', + ); + expect(mockLoggerInfo).toHaveBeenCalledWith( + expect.objectContaining({ functionName, statusCode: 202, - }, + }), + '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', ); }); @@ -406,11 +417,11 @@ describe('lambda-client', () => { // Assert expect(mockSend).toHaveBeenCalled(); expect(mockLoggerInfo).toHaveBeenCalledWith( - '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', expect.objectContaining({ functionName, statusCode: 202, }), + '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', ); }); @@ -430,11 +441,11 @@ describe('lambda-client', () => { // Assert expect(mockLoggerInfo).toHaveBeenCalledWith( - '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', expect.objectContaining({ functionName, statusCode: 202, }), + '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', ); }); @@ -453,14 +464,14 @@ describe('lambda-client', () => { // Act & Assert await expect(invokeLambdaAsync(functionName, payload)).rejects.toThrow('Lambda function error: Unhandled'); expect(mockLoggerError).toHaveBeenCalledWith( - '[LambdaClient] < invokeLambdaAsync - Lambda function returned an error', - expect.any(Error), expect.objectContaining({ functionName, + FunctionError: 'Unhandled', response: expect.objectContaining({ FunctionError: 'Unhandled', }), }), + '[LambdaClient] < invokeLambdaAsync - Lambda function returned an error', ); }); @@ -475,9 +486,11 @@ describe('lambda-client', () => { // Act & Assert await expect(invokeLambdaAsync(functionName, payload)).rejects.toThrow('Function not found'); expect(mockLoggerError).toHaveBeenCalledWith( + expect.objectContaining({ + error, + functionName, + }), '[LambdaClient] < invokeLambdaAsync - failed to invoke Lambda function', - error, - { functionName }, ); }); @@ -502,11 +515,11 @@ describe('lambda-client', () => { // Assert - invokeLambdaAsync returns void expect(mockLoggerInfo).toHaveBeenCalledWith( - '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', expect.objectContaining({ functionName, statusCode: 202, }), + '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', ); }); @@ -526,8 +539,8 @@ describe('lambda-client', () => { // Assert expect(mockLoggerDebug).toHaveBeenCalledWith( - '[LambdaClient] invokeLambdaAsync - InvokeCommand', expect.any(Object), + '[LambdaClient] invokeLambdaAsync - InvokeCommand', ); }); @@ -562,11 +575,11 @@ describe('lambda-client', () => { // Assert expect(mockLoggerInfo).toHaveBeenCalledWith( - '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', expect.objectContaining({ functionName, statusCode: 202, }), + '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', ); }); @@ -603,11 +616,11 @@ describe('lambda-client', () => { await invokeLambdaAsync(functionName, payload); expect(mockLoggerInfo).toHaveBeenCalledWith( - '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', expect.objectContaining({ functionName, statusCode: 202, }), + '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', ); }); @@ -632,11 +645,11 @@ describe('lambda-client', () => { // Assert expect(mockLoggerInfo).toHaveBeenCalledWith( - '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', expect.objectContaining({ functionName, statusCode: 202, }), + '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', ); }); @@ -655,17 +668,17 @@ describe('lambda-client', () => { await invokeLambdaAsync(functionName, payload); // Assert - expect(mockLoggerInfo).toHaveBeenCalledWith('[LambdaClient] > invokeLambdaAsync', { functionName }); + expect(mockLoggerInfo).toHaveBeenCalledWith({ functionName }, '[LambdaClient] > invokeLambdaAsync'); expect(mockLoggerDebug).toHaveBeenCalledWith( - '[LambdaClient] invokeLambdaAsync - InvokeCommand', expect.any(Object), + '[LambdaClient] invokeLambdaAsync - InvokeCommand', ); expect(mockLoggerInfo).toHaveBeenCalledWith( - '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', { functionName, statusCode: 202, }, + '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', ); }); }); diff --git a/src/utils/lambda-client.ts b/src/utils/lambda-client.ts index 0ae55fa..7184ed0 100644 --- a/src/utils/lambda-client.ts +++ b/src/utils/lambda-client.ts @@ -21,46 +21,48 @@ export type JsonPayload = Record | Array; * @throws Error if the Lambda invocation fails */ export const invokeLambdaSync = async (functionName: string, payload: JsonPayload): Promise => { - logger.info('[LambdaClient] > invokeLambdaSync', { functionName }); + logger.info({ functionName }, '[LambdaClient] > invokeLambdaSync'); try { + // Create the InvokeCommand with RequestResponse invocation type const command = new InvokeCommand({ FunctionName: functionName, InvocationType: 'RequestResponse', Payload: JSON.stringify(payload), }); - logger.debug('[LambdaClient] invokeLambdaSync - InvokeCommand', { command }); + logger.debug({ command }, '[LambdaClient] invokeLambdaSync - InvokeCommand'); + // Send the command to invoke the Lambda function const response = await _lambdaClient.send(command); // Parse the response payload const responsePayload = response.Payload ? JSON.parse(new TextDecoder().decode(response.Payload)) : null; - logger.info('[LambdaClient] < invokeLambdaSync - successfully invoked Lambda function', { - functionName, - statusCode: response.StatusCode, - }); - // Check for function errors if (response.FunctionError) { logger.error( - '[LambdaClient] < invokeLambdaSync - Lambda function returned an error', - new Error(response.FunctionError), { functionName, FunctionError: response.FunctionError, responsePayload, }, + '[LambdaClient] < invokeLambdaSync - Lambda function returned an error', ); throw new Error(`Lambda function error: ${response.FunctionError}`); } + logger.info( + { functionName, statusCode: response.StatusCode }, + '[LambdaClient] < invokeLambdaSync - successfully invoked Lambda function', + ); return responsePayload as T; } catch (error) { - logger.error('[LambdaClient] < invokeLambdaSync - failed to invoke Lambda function', error as Error, { - functionName, - }); + // Handle invocation errors + logger.error( + { error: error as Error, functionName }, + '[LambdaClient] < invokeLambdaSync - failed to invoke Lambda function', + ); throw error; } }; @@ -72,40 +74,40 @@ export const invokeLambdaSync = async (functionName: string, payloa * @throws Error if the Lambda invocation fails */ export const invokeLambdaAsync = async (functionName: string, payload: JsonPayload): Promise => { - logger.info('[LambdaClient] > invokeLambdaAsync', { functionName }); + logger.info({ functionName }, '[LambdaClient] > invokeLambdaAsync'); try { + // Create the InvokeCommand with Event invocation type const command = new InvokeCommand({ FunctionName: functionName, InvocationType: 'Event', Payload: JSON.stringify(payload), }); - logger.debug('[LambdaClient] invokeLambdaAsync - InvokeCommand', { command }); + logger.debug({ command }, '[LambdaClient] invokeLambdaAsync - InvokeCommand'); + // Send the command to invoke the Lambda function const response = await _lambdaClient.send(command); // Check for function errors if (response.FunctionError) { logger.error( + { functionName, FunctionError: response.FunctionError, response }, '[LambdaClient] < invokeLambdaAsync - Lambda function returned an error', - new Error(response.FunctionError), - { - functionName, - response, - }, ); throw new Error(`Lambda function error: ${response.FunctionError}`); } - logger.info('[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', { - functionName, - statusCode: response.StatusCode, - }); + logger.info( + { functionName, statusCode: response.StatusCode }, + '[LambdaClient] < invokeLambdaAsync - successfully invoked Lambda function', + ); } catch (error) { - logger.error('[LambdaClient] < invokeLambdaAsync - failed to invoke Lambda function', error as Error, { - functionName, - }); + // Handle invocation errors + logger.error( + { error: error as Error, functionName }, + '[LambdaClient] < invokeLambdaAsync - failed to invoke Lambda function', + ); throw error; } }; diff --git a/src/utils/logger.test.ts b/src/utils/logger.test.ts index d6b8a1f..8024624 100644 --- a/src/utils/logger.test.ts +++ b/src/utils/logger.test.ts @@ -1,9 +1,6 @@ describe('logger', () => { let logger: typeof import('./logger').logger; - let mockDebug: jest.SpyInstance; - let mockInfo: jest.SpyInstance; - let mockWarn: jest.SpyInstance; - let mockError: jest.SpyInstance; + let stdoutSpy: jest.SpyInstance; // Helper to mock config before importing logger function setConfig(overrides: Partial<{ LOGGING_ENABLED: boolean; LOGGING_LEVEL: string; LOGGING_FORMAT: string }>) { @@ -21,30 +18,27 @@ describe('logger', () => { beforeEach(() => { jest.resetModules(); - mockDebug = jest.spyOn(console, 'debug').mockImplementation(() => {}); - mockInfo = jest.spyOn(console, 'info').mockImplementation(() => {}); - mockWarn = jest.spyOn(console, 'warn').mockImplementation(() => {}); - mockError = jest.spyOn(console, 'error').mockImplementation(() => {}); + stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true); }); afterEach(() => { - mockDebug.mockRestore(); - mockInfo.mockRestore(); - mockWarn.mockRestore(); - mockError.mockRestore(); + stdoutSpy.mockRestore(); jest.resetModules(); jest.clearAllMocks(); }); it('logs debug when enabled and level is debug', () => { // Arrange - setConfig({ LOGGING_ENABLED: true, LOGGING_LEVEL: 'debug', LOGGING_FORMAT: 'text' }); + setConfig({ LOGGING_ENABLED: true, LOGGING_LEVEL: 'debug', LOGGING_FORMAT: 'json' }); // Act - logger.debug('debug message', { foo: 'bar' }); + logger.debug({ foo: 'bar' }, 'debug message'); // Assert - expect(mockDebug).toHaveBeenCalledWith('debug message', { foo: 'bar' }); + expect(stdoutSpy).toHaveBeenCalled(); + const output = stdoutSpy.mock.calls.map((call) => call[0] as string).join(''); + expect(output).toContain('debug message'); + expect(output).toContain('foo'); }); it('does not log debug if level is info', () => { @@ -55,18 +49,20 @@ describe('logger', () => { logger.debug('should not log'); // Assert - expect(mockDebug).not.toHaveBeenCalled(); + expect(stdoutSpy).not.toHaveBeenCalled(); }); it('logs info when enabled and level is info', () => { // Arrange - setConfig({ LOGGING_ENABLED: true, LOGGING_LEVEL: 'info', LOGGING_FORMAT: 'text' }); + setConfig({ LOGGING_ENABLED: true, LOGGING_LEVEL: 'info', LOGGING_FORMAT: 'json' }); // Act logger.info('info message'); // Assert - expect(mockInfo).toHaveBeenCalledWith('info message', undefined); + expect(stdoutSpy).toHaveBeenCalled(); + const output = stdoutSpy.mock.calls.map((call) => call[0] as string).join(''); + expect(output).toContain('info message'); }); it('does not log info if level is warn', () => { @@ -77,45 +73,50 @@ describe('logger', () => { logger.info('should not log'); // Assert - expect(mockInfo).not.toHaveBeenCalled(); + expect(stdoutSpy).not.toHaveBeenCalled(); }); it('logs warn when enabled and level is warn', () => { // Arrange - setConfig({ LOGGING_ENABLED: true, LOGGING_LEVEL: 'warn', LOGGING_FORMAT: 'text' }); + setConfig({ LOGGING_ENABLED: true, LOGGING_LEVEL: 'warn', LOGGING_FORMAT: 'json' }); // Act - logger.warn('warn message', { a: 1 }); + logger.warn({ a: 1 }, 'warn message'); // Assert - expect(mockWarn).toHaveBeenCalledWith('warn message', { a: 1 }); + expect(stdoutSpy).toHaveBeenCalled(); + const output = stdoutSpy.mock.calls.map((call) => call[0] as string).join(''); + expect(output).toContain('warn message'); + expect(output).toContain('a'); }); it('logs error with error object', () => { // Arrange - setConfig({ LOGGING_ENABLED: true, LOGGING_LEVEL: 'error', LOGGING_FORMAT: 'text' }); + setConfig({ LOGGING_ENABLED: true, LOGGING_LEVEL: 'error', LOGGING_FORMAT: 'json' }); const error = new Error('fail'); // Act - logger.error('error message', error, { foo: 1 }); + logger.error({ error, foo: 1 }, 'error message'); // Assert - expect(mockError).toHaveBeenCalled(); - const errorContext = mockError.mock.calls[0][1] as Record; - expect(errorContext.errorMessage).toBe('fail'); - expect(errorContext.foo).toBe(1); - expect(errorContext.stack).toBeDefined(); + expect(stdoutSpy).toHaveBeenCalled(); + const output = stdoutSpy.mock.calls.map((call) => call[0] as string).join(''); + expect(output).toContain('error message'); + expect(output).toContain('error'); + expect(output).toContain('foo'); }); it('logs error without error object', () => { // Arrange - setConfig({ LOGGING_ENABLED: true, LOGGING_LEVEL: 'error', LOGGING_FORMAT: 'text' }); + setConfig({ LOGGING_ENABLED: true, LOGGING_LEVEL: 'error', LOGGING_FORMAT: 'json' }); // Act logger.error('error message'); // Assert - expect(mockError).toHaveBeenCalledWith('error message', undefined); + expect(stdoutSpy).toHaveBeenCalled(); + const output = stdoutSpy.mock.calls.map((call) => call[0] as string).join(''); + expect(output).toContain('error message'); }); it('does not log if LOGGING_ENABLED is false', () => { @@ -129,29 +130,16 @@ describe('logger', () => { logger.error('should not log'); // Assert - expect(mockDebug).not.toHaveBeenCalled(); - expect(mockInfo).not.toHaveBeenCalled(); - expect(mockWarn).not.toHaveBeenCalled(); - expect(mockError).not.toHaveBeenCalled(); + expect(stdoutSpy).not.toHaveBeenCalled(); }); describe('JSON format', () => { - let stdoutSpy: jest.SpyInstance; - - beforeEach(() => { - stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true); - }); - - afterEach(() => { - stdoutSpy.mockRestore(); - }); - it('logs as JSON when LOGGING_FORMAT is json', () => { // Arrange setConfig({ LOGGING_ENABLED: true, LOGGING_LEVEL: 'info', LOGGING_FORMAT: 'json' }); // Act - logger.info('test message', { userId: 123 }); + logger.info({ userId: 123 }, 'test message'); // Assert expect(stdoutSpy).toHaveBeenCalled(); @@ -166,7 +154,7 @@ describe('logger', () => { setConfig({ LOGGING_ENABLED: true, LOGGING_LEVEL: 'debug', LOGGING_FORMAT: 'json' }); // Act - logger.debug('processing request', { requestId: 'abc-123', duration: 250 }); + logger.debug({ requestId: 'abc-123', duration: 250 }, 'processing request'); // Assert expect(stdoutSpy).toHaveBeenCalled(); @@ -183,14 +171,14 @@ describe('logger', () => { const error = new Error('test error'); // Act - logger.error('operation failed', error, { operation: 'getData' }); + logger.error({ error, operation: 'getData' }, 'operation failed'); // Assert expect(stdoutSpy).toHaveBeenCalled(); // Verify that JSON output was written with error details const allOutput = stdoutSpy.mock.calls.map((call) => call[0] as string).join(''); expect(allOutput).toContain('operation failed'); - expect(allOutput).toContain('test error'); + expect(allOutput).toContain('error'); expect(allOutput).toContain('getData'); }); }); @@ -201,29 +189,27 @@ describe('logger', () => { setConfig({ LOGGING_ENABLED: true, LOGGING_LEVEL: 'info', LOGGING_FORMAT: 'text' }); // Act - logger.info('test message', { userId: 123 }); + logger.info({ userId: 123 }, 'test message'); // Assert - expect(mockInfo).toHaveBeenCalled(); - const message = mockInfo.mock.calls[0][0] as string; - const context = mockInfo.mock.calls[0][1] as Record; - expect(message).toBe('test message'); - expect(context).toEqual({ userId: 123 }); + expect(stdoutSpy).toHaveBeenCalled(); + const output = stdoutSpy.mock.calls.map((call) => call[0] as string).join(''); + expect(output).toContain('test message'); + expect(output).toContain('userId'); }); - it('logs with context as stringified object in text format', () => { + it('logs with context as separate fields in text format', () => { // Arrange setConfig({ LOGGING_ENABLED: true, LOGGING_LEVEL: 'warn', LOGGING_FORMAT: 'text' }); // Act - logger.warn('warning message', { code: 'WARN_001' }); + logger.warn({ code: 'WARN_001' }, 'warning message'); // Assert - expect(mockWarn).toHaveBeenCalled(); - const message = mockWarn.mock.calls[0][0] as string; - const context = mockWarn.mock.calls[0][1] as Record; - expect(message).toBe('warning message'); - expect(context).toEqual({ code: 'WARN_001' }); + expect(stdoutSpy).toHaveBeenCalled(); + const output = stdoutSpy.mock.calls.map((call) => call[0] as string).join(''); + expect(output).toContain('warning message'); + expect(output).toContain('WARN_001'); }); it('logs error with error details in text format', () => { @@ -232,15 +218,13 @@ describe('logger', () => { const error = new Error('test error'); // Act - logger.error('operation failed', error); + logger.error({ error }, 'operation failed'); // Assert - expect(mockError).toHaveBeenCalled(); - const message = mockError.mock.calls[0][0] as string; - const context = mockError.mock.calls[0][1] as Record; - expect(message).toBe('operation failed'); - expect(context.errorMessage).toBe('test error'); - expect(context.stack).toBeDefined(); + expect(stdoutSpy).toHaveBeenCalled(); + const output = stdoutSpy.mock.calls.map((call) => call[0] as string).join(''); + expect(output).toContain('operation failed'); + expect(output).toContain('error'); }); }); }); diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 257a2f8..d338eaf 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,5 +1,5 @@ import pino from 'pino'; -import { pinoLambdaDestination, StructuredLogFormatter } from 'pino-lambda'; +import { CloudwatchLogFormatter, pinoLambdaDestination, StructuredLogFormatter } from 'pino-lambda'; import { config } from './config'; @@ -8,121 +8,16 @@ import { config } from './config'; * @see https://www.npmjs.com/package/pino-lambda#best-practices */ const _lambdaDestination = pinoLambdaDestination({ - formatter: new StructuredLogFormatter(), + formatter: config.LOGGING_FORMAT === 'json' ? new StructuredLogFormatter() : new CloudwatchLogFormatter(), }); /** * Pino logger instance */ -const _logger = pino( +export const logger = pino( { + enabled: config.LOGGING_ENABLED, level: config.LOGGING_LEVEL, }, _lambdaDestination, ); - -/** - * Log levels with numeric values for comparison - */ -const LOG_LEVELS = { - debug: 0, - info: 1, - warn: 2, - error: 3, -} as const; - -type LogLevel = keyof typeof LOG_LEVELS; - -/** - * Check if a given log level should be logged based on configured level - */ -const _shouldLog = (level: LogLevel): boolean => { - if (!config.LOGGING_ENABLED) return false; - - const configuredLevel = LOG_LEVELS[config.LOGGING_LEVEL as LogLevel]; - const requestedLevel = LOG_LEVELS[level]; - - return requestedLevel >= configuredLevel; -}; - -/** - * Internal logging function that logs messages in text format. - */ -const _logText = (level: LogLevel, message: string, context?: Record): void => { - switch (level) { - case 'debug': - console.debug(message, context); - break; - case 'info': - console.info(message, context); - break; - case 'warn': - console.warn(message, context); - break; - case 'error': - console.error(message, context); - break; - } -}; - -/** - * Internal logging function that logs messages in json format. - */ -const _logJson = (level: LogLevel, message: string, context?: Record): void => { - switch (level) { - case 'debug': - _logger.debug({ context }, message); - break; - case 'info': - _logger.info({ context }, message); - break; - case 'warn': - _logger.warn({ context }, message); - break; - case 'error': - _logger.error({ context }, message); - break; - } -}; - -/** - * Internal logging function that routes to appropriate formatter. - */ -const _log = (level: LogLevel, message: string, context?: Record): void => { - if (!_shouldLog(level)) return; - - if (config.LOGGING_FORMAT === 'json') { - _logJson(level, message, context); - } else { - _logText(level, message, context); - } -}; - -/** - * Logger utility configured based on environment settings - */ -export const logger = { - debug(message: string, context?: Record): void { - _log('debug', message, context); - }, - - info(message: string, context?: Record): void { - _log('info', message, context); - }, - - warn(message: string, context?: Record): void { - _log('warn', message, context); - }, - - error(message: string, error?: Error, context?: Record): void { - const errorContext = error - ? { - ...context, - errorMessage: error.message, - stack: error.stack, - } - : context; - - _log('error', message, errorContext); - }, -}; diff --git a/src/utils/sns-client.test.ts b/src/utils/sns-client.test.ts index 05732be..fdb4341 100644 --- a/src/utils/sns-client.test.ts +++ b/src/utils/sns-client.test.ts @@ -66,7 +66,10 @@ describe('sns-client', () => { // Assert expect(result).toBe('message-id-123'); expect(mockSend).toHaveBeenCalledTimes(1); - expect(mockLoggerDebug).toHaveBeenCalledWith('[SnsClient] > publishToTopic', { topicArn }); + expect(mockLoggerDebug).toHaveBeenCalledWith( + expect.objectContaining({ topicArn }), + '[SnsClient] > publishToTopic', + ); }); it('should convert message object to JSON string', async () => { @@ -138,10 +141,13 @@ describe('sns-client', () => { await publishToTopic(topicArn, message); // Assert - expect(mockLoggerDebug).toHaveBeenCalledWith('[SnsClient] < publishToTopic - successfully published message', { - topicArn, - messageId: 'msg-delete-123', - }); + expect(mockLoggerDebug).toHaveBeenCalledWith( + expect.objectContaining({ + topicArn, + messageId: 'msg-delete-123', + }), + '[SnsClient] < publishToTopic - successfully published message', + ); }); it('should handle SNS publish errors and rethrow them', async () => { @@ -155,11 +161,11 @@ describe('sns-client', () => { // Act & Assert await expect(publishToTopic(topicArn, message)).rejects.toThrow('SNS publish failed'); expect(mockLoggerError).toHaveBeenCalledWith( - '[SnsClient] < publishToTopic - failed to publish message to SNS', - mockError, - { + expect.objectContaining({ + error: mockError, topicArn, - }, + }), + '[SnsClient] < publishToTopic - failed to publish message to SNS', ); }); diff --git a/src/utils/sns-client.ts b/src/utils/sns-client.ts index 6566159..0c0ee24 100644 --- a/src/utils/sns-client.ts +++ b/src/utils/sns-client.ts @@ -30,29 +30,33 @@ export const publishToTopic = async ( message: Record, attributes?: MessageAttributes, ): Promise => { - logger.debug('[SnsClient] > publishToTopic', { topicArn }); + logger.debug({ topicArn }, '[SnsClient] > publishToTopic'); try { + // Create the PublishCommand with the message and attributes const command = new PublishCommand({ TopicArn: topicArn, Message: JSON.stringify(message), MessageAttributes: attributes, }); - logger.debug('[SnsClient] publishToTopic - PublishCommand', { command }); + logger.debug({ command }, '[SnsClient] publishToTopic - PublishCommand'); + // Send the publish command const response = await _snsClient.send(command); - logger.debug('[SnsClient] < publishToTopic - successfully published message', { - topicArn, - messageId: response.MessageId, - }); + logger.debug( + { topicArn, messageId: response.MessageId }, + '[SnsClient] < publishToTopic - successfully published message', + ); return response.MessageId ?? ''; } catch (error) { - logger.error('[SnsClient] < publishToTopic - failed to publish message to SNS', error as Error, { - topicArn, - }); + // Handle publish errors + logger.error( + { error: error as Error, topicArn }, + '[SnsClient] < publishToTopic - failed to publish message to SNS', + ); throw error; } };