├── .github └── workflows │ ├── codeql-analysis.yml │ └── tfsec.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .terraform-version ├── .terraform.lock.hcl ├── README.md ├── data.tf ├── main.tf ├── outputs.tf ├── providers.tf ├── sources ├── .gitignore ├── .node-version ├── package-lock.json ├── package.json ├── src │ ├── decorators │ │ └── statistic.decorator.ts │ ├── functions │ │ ├── booking-create.function.ts │ │ ├── booking-search.function.ts │ │ └── flight-search.function.ts │ ├── helpers │ │ ├── chunk.helper.ts │ │ └── parameter-store.helper.ts │ ├── index.ts │ ├── libraries │ │ ├── datadog.library.ts │ │ └── dynamodb.library.ts │ └── models │ │ ├── booking.model.ts │ │ ├── flight.model.ts │ │ ├── model.ts │ │ └── transaction.model.ts └── tsconfig.json ├── terraform.tfvars.example └── variables.tf /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '23 10 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v2 71 | -------------------------------------------------------------------------------- /.github/workflows/tfsec.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: tfsec 7 | 8 | on: 9 | push: 10 | branches: [ main ] 11 | pull_request: 12 | branches: [ main ] 13 | schedule: 14 | - cron: '41 6 * * 6' 15 | 16 | jobs: 17 | tfsec: 18 | name: Run tfsec sarif report 19 | runs-on: ubuntu-latest 20 | permissions: 21 | actions: read 22 | contents: read 23 | security-events: write 24 | 25 | steps: 26 | - name: Clone repo 27 | uses: actions/checkout@v3 28 | 29 | - name: Run tfsec 30 | uses: tfsec/tfsec-sarif-action@9a83b5c3524f825c020e356335855741fd02745f 31 | with: 32 | sarif_file: tfsec.sarif 33 | 34 | - name: Upload SARIF file 35 | uses: github/codeql-action/upload-sarif@v2 36 | with: 37 | # Path to SARIF file relative to the root of the repository 38 | sarif_file: tfsec.sarif 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .terraform 2 | *.tfstate 3 | *.backup 4 | *.lock.info 5 | *.tfvars -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/terraform-docs/terraform-docs 3 | rev: "v0.16.0" 4 | hooks: 5 | - id: terraform-docs-go 6 | args: ["markdown", "table", "--output-file", "README.md", "."] 7 | -------------------------------------------------------------------------------- /.terraform-version: -------------------------------------------------------------------------------- 1 | 1.1.9 2 | -------------------------------------------------------------------------------- /.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/archive" { 5 | version = "2.2.0" 6 | hashes = [ 7 | "h1:CIWi5G6ob7p2wWoThRQbOB8AbmFlCzp7Ka81hR3cVp0=", 8 | "zh:06bd875932288f235c16e2237142b493c2c2b6aba0e82e8c85068332a8d2a29e", 9 | "zh:0c681b481372afcaefddacc7ccdf1d3bb3a0c0d4678a526bc8b02d0c331479bc", 10 | "zh:100fc5b3fc01ea463533d7bbfb01cb7113947a969a4ec12e27f5b2be49884d6c", 11 | "zh:55c0d7ddddbd0a46d57c51fcfa9b91f14eed081a45101dbfc7fd9d2278aa1403", 12 | "zh:73a5dd68379119167934c48afa1101b09abad2deb436cd5c446733e705869d6b", 13 | "zh:841fc4ac6dc3479981330974d44ad2341deada8a5ff9e3b1b4510702dfbdbed9", 14 | "zh:91be62c9b41edb137f7f835491183628d484e9d6efa82fcb75cfa538c92791c5", 15 | "zh:acd5f442bd88d67eb948b18dc2ed421c6c3faee62d3a12200e442bfff0aa7d8b", 16 | "zh:ad5720da5524641ad718a565694821be5f61f68f1c3c5d2cfa24426b8e774bef", 17 | "zh:e63f12ea938520b3f83634fc29da28d92eed5cfbc5cc8ca08281a6a9c36cca65", 18 | "zh:f6542918faa115df46474a36aabb4c3899650bea036b5f8a5e296be6f8f25767", 19 | ] 20 | } 21 | 22 | provider "registry.terraform.io/hashicorp/aws" { 23 | version = "4.10.0" 24 | constraints = "4.10.0" 25 | hashes = [ 26 | "h1:S6xGPRL08YEuBdemiYZyIBf/YwM4OCvzVuaiuU6kLjc=", 27 | "zh:0a2a7eabfeb7dbb17b7f82aff3fa2ba51e836c15e5be4f5468ea44bd1299b48d", 28 | "zh:23409c7205d13d2d68b5528e1c49e0a0455d99bbfec61eb0201142beffaa81f7", 29 | "zh:3adad2245d97816f3919778b52c58fb2de130938a3e9081358bfbb72ec478d9a", 30 | "zh:5bf100aba6332f24b1ffeae7536d5d489bb907bf774a06b95f2183089eaf1a1a", 31 | "zh:63c3a24c0c229a1d3390e6ea2454ba4d8ace9b94e086bee1dbdcf665ae969e15", 32 | "zh:6b76f5ffd920f0a750da3a4ff1d00eab18d9cd3731b009aae3df4135613bad4d", 33 | "zh:8cd6b1e6b51e8e9bbe2944bb169f113d20d1d72d07ccd1b7b83f40b3c958233e", 34 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 35 | "zh:c5c31f58fb5bd6aebc6c662a4693640ec763cb3399cce0b592101cf24ece1625", 36 | "zh:cc485410be43d6ad95d81b9e54cc4d2117aadf9bf5941165a9df26565d9cce42", 37 | "zh:cebb89c74b6a3dc6780824b1d1e2a8d16a51e75679e14ad0b830d9f7da1a3a67", 38 | "zh:e7dc427189cb491e1f96e295101964415cbf8630395ee51e396d2a811f365237", 39 | ] 40 | } 41 | 42 | provider "registry.terraform.io/hashicorp/null" { 43 | version = "3.1.1" 44 | hashes = [ 45 | "h1:71sNUDvmiJcijsvfXpiLCz0lXIBSsEJjMxljt7hxMhw=", 46 | "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", 47 | "zh:08c058e367de6debdad35fc24d97131c7cf75103baec8279aba3506a08b53faf", 48 | "zh:73ce6dff935150d6ddc6ac4a10071e02647d10175c173cfe5dca81f3d13d8afe", 49 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 50 | "zh:8fdd792a626413502e68c195f2097352bdc6a0df694f7df350ed784741eb587e", 51 | "zh:976bbaf268cb497400fd5b3c774d218f3933271864345f18deebe4dcbfcd6afa", 52 | "zh:b21b78ca581f98f4cdb7a366b03ae9db23a73dfa7df12c533d7c19b68e9e72e5", 53 | "zh:b7fc0c1615dbdb1d6fd4abb9c7dc7da286631f7ca2299fb9cd4664258ccfbff4", 54 | "zh:d1efc942b2c44345e0c29bc976594cb7278c38cfb8897b344669eafbc3cddf46", 55 | "zh:e356c245b3cd9d4789bab010893566acace682d7db877e52d40fc4ca34a50924", 56 | "zh:ea98802ba92fcfa8cf12cbce2e9e7ebe999afbf8ed47fa45fc847a098d89468b", 57 | "zh:eff8872458806499889f6927b5d954560f3d74bf20b6043409edf94d26cd906f", 58 | ] 59 | } 60 | 61 | provider "registry.terraform.io/hashicorp/random" { 62 | version = "3.1.2" 63 | hashes = [ 64 | "h1:5A5VsY5wNmOZlupUcLnIoziMPn8htSZBXbP3lI7lBEM=", 65 | "zh:0daceba867b330d3f8e2c5dc895c4291845a78f31955ce1b91ab2c4d1cd1c10b", 66 | "zh:104050099efd30a630741f788f9576b19998e7a09347decbec3da0b21d64ba2d", 67 | "zh:173f4ef3fdf0c7e2564a3db0fac560e9f5afdf6afd0b75d6646af6576b122b16", 68 | "zh:41d50f975e535f968b3f37170fb07937c15b76d85ba947d0ce5e5ff9530eda65", 69 | "zh:51a5038867e5e60757ed7f513dd6a973068241190d158a81d1b69296efb9cb8d", 70 | "zh:6432a568e97a5a36cc8aebca5a7e9c879a55d3bc71d0da1ab849ad905f41c0be", 71 | "zh:6bac6501394b87138a5e17c9f3a41e46ff7833ad0ba2a96197bb7787e95b641c", 72 | "zh:6c0a7f5faacda644b022e7718e53f5868187435be6d000786d1ca05aa6683a25", 73 | "zh:74c89de3fa6ef3027efe08f8473c2baeb41b4c6cee250ba7aeb5b64e8c79800d", 74 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 75 | "zh:b29eabbf0a5298f0e95a1df214c7cfe06ea9bcf362c63b3ad2f72d85da7d4685", 76 | "zh:e891458c7a61e5b964e09616f1a4f87d0471feae1ec04cc51776e7dec1a3abce", 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boilerplate Lambda Typescript 2 | 3 | ## Overview 4 | 5 | This is a boilerplate to help you initiate AWS Lambda project using Typescript, in this boilerplate there are `terraform code` to provision the stacks and the initial Typescript source code in the `sources` directory 6 | 7 | ### Features 8 | 9 | - Terraform code to provision the AWS Lambda project 10 | - Typescript source code in the `sources` directory 11 | - Automatically load AWS Secrets Manager (parameter store) as environment variables 12 | - Automatically load DynamoDB (table name) as environment variables 13 | - Automatically create models for DynamoDB tables with the ability to read, write, delete, and scan 14 | - Decorator example to log the execution time of the method 15 | - Datadog example integration to stream the metrics of statistic decorator to Datadog 16 | - Lambda layer to store the dependencies of the project 17 | - Lambda scheduler to schedule the function invocation 18 | 19 | ### Project Structures 20 | 21 | ``` 22 | . 23 | ├── sources 24 | │ ├── package-lock.json 25 | │ ├── package.json 26 | │ ├── src 27 | │ │ ├── decorators 28 | │ │ │ └── statistic.decorator.ts 29 | │ │ ├── functions 30 | │ │ │ ├── booking-create.function.ts 31 | │ │ │ ├── booking-search.function.ts 32 | │ │ │ └── flight-search.function.ts 33 | │ │ ├── helpers 34 | │ │ │ ├── chunk.helper.ts 35 | │ │ │ └── parameter-store.helper.ts 36 | │ │ ├── index.ts 37 | │ │ ├── libraries 38 | │ │ │ ├── datadog.library.ts 39 | │ │ │ └── dynamodb.library.ts 40 | │ │ └── models 41 | │ │ ├── booking.model.ts 42 | │ │ ├── flight.model.ts 43 | │ │ ├── model.ts 44 | │ │ └── transaction.model.ts 45 | │ └── tsconfig.json 46 | ├── README.md 47 | ├── data.tf 48 | ├── main.tf 49 | ├── providers.tf 50 | ├── terraform.tfvars.example 51 | └── variables.tf 52 | ``` 53 | 54 | While doing a `terraform apply` command, theese are the things that will be created: 55 | 56 | - AWS Lambda Function, in the `main.tf` there's a logic on creating AWS Lambda function based on files with format `*.function.ts` under `sources/src/functions` directory, so the number of AWS Lambda function created is based on `*.function.ts` files 57 | - AWS System Manager Parameter Store, in the `main.tf` there's a logic on creating AWS Parameter Store with prefix set on `parameter_store_path` under `variables.tf` based on: 58 | - **parameter_store_list** attributes under `variables.tf` file 59 | - **dynamodb_table_list** attributes under `variables.tf` file which will create a Parameter Store to store the table names of DynamoDB with format `dynamodb-table-{table_name}` 60 | - **service_version** attributes under `variables.tf` file which will create a Parameter Store to store the version of the service 61 | 62 | Another context related to the Typescript source code: 63 | 64 | - **sources/src/helpers** is the collection of functional helpers such as `populateEnvironmentVariables()` you can freely add another functional helpers under this directory 65 | - **sources/src/libraries** is the collection of class helpers such as `DatadogLibrary` which contains all Datadog functionality such as `publishMetrics` and `publishEvents`, or another example `DynamoDBLibrary` which contains `putItem` and `getItem` 66 | - **sources/src/decorators** is the collection of Typescript decorators, the initial example is `@statistic` decorator which have the functionality to log the execution duration for the method that uses the decorators, the example also include the additional process to stream the statistic metrics into Datadog 67 | - **sources/src/index.ts** is a bootstraper file which contains default `exports.handlers` function, which is the default function that will be called by AWS Lambda Function, this file contains logic to create the `sources/src/functions/*.function.ts` instance and create object then call the `handler` method 68 | - **sources/src/models** is the collection of Typescript models, the initial example is `Booking` which is the model for DynamoDB table `booking` which automatically created by terraform code 69 | 70 | 71 | ## Requirements 72 | 73 | | Name | Version | 74 | |------|---------| 75 | | [terraform](#requirement\_terraform) | ~> 1.1.9 | 76 | | [aws](#requirement\_aws) | ~> 4.10.0 | 77 | 78 | ## Providers 79 | 80 | | Name | Version | 81 | |------|---------| 82 | | [archive](#provider\_archive) | 2.2.0 | 83 | | [aws](#provider\_aws) | 4.10.0 | 84 | | [null](#provider\_null) | 3.1.1 | 85 | 86 | ## Modules 87 | 88 | | Name | Source | Version | 89 | |------|--------|---------| 90 | | [dynamodb-table-name](#module\_dynamodb-table-name) | git@github.com:traveloka/terraform-aws-resource-naming.git | v0.22.0 | 91 | | [lambda-function-name](#module\_lambda-function-name) | git@github.com:traveloka/terraform-aws-resource-naming.git | v0.22.0 | 92 | | [lambda-layer-name](#module\_lambda-layer-name) | git@github.com:traveloka/terraform-aws-resource-naming.git | v0.22.0 | 93 | 94 | ## Resources 95 | 96 | | Name | Type | 97 | |------|------| 98 | | [aws_cloudwatch_event_rule.lambda-function-trigger-schedule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource | 99 | | [aws_cloudwatch_event_target.lambda-function-trigger-schedule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource | 100 | | [aws_cloudwatch_log_group.lambda-function-log-group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | 101 | | [aws_dynamodb_table.dynamodb-table](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource | 102 | | [aws_iam_role.lambda-function-role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 103 | | [aws_iam_role_policy.function-policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | 104 | | [aws_kms_alias.service-alias](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource | 105 | | [aws_kms_key.service-key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | 106 | | [aws_lambda_function.lambda-function](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | 107 | | [aws_lambda_layer_version.lambda-layer](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_layer_version) | resource | 108 | | [aws_lambda_permission.lambda-function-trigger-schedule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | 109 | | [aws_ssm_parameter.ssm-parameter-custom](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 110 | | [aws_ssm_parameter.ssm-parameter-dynamodb-table](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 111 | | [aws_ssm_parameter.ssm-parameter-service-version](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | 112 | | [null_resource.lambda-function-source-builder](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | 113 | | [null_resource.lambda-layer-source-builder](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | 114 | | [null_resource.typescript-source-model-builder](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | 115 | | [archive_file.lambda-function-source](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | 116 | | [archive_file.lambda-layer-source](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | 117 | | [archive_file.typescript-source](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | 118 | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | 119 | | [aws_iam_policy_document.function-policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 120 | | [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | 121 | 122 | ## Inputs 123 | 124 | | Name | Description | Type | Default | Required | 125 | |------|-------------|------|---------|:--------:| 126 | | [default\_tags](#input\_default\_tags) | The default tags for the service | `map(string)` | `{}` | no | 127 | | [dynamodb\_table\_list](#input\_dynamodb\_table\_list) | The list of dynamodb tables to be used for the service, e.g.
[
  {
    "name": "booking",
    "key": "id"
  },
  {
    "name": "flight",
    "key": "id"
  },
  {
    "name": "transaction",
    "key": "booking\_id",
    "range\_key": "flight\_id"
  }
]
|
list(object({
name = string,
key = string,
range_key = optional(string),
}))
| `[]` | no | 128 | | [lambda\_function\_configuration](#input\_lambda\_function\_configuration) | The custom configuration for the Lambda Function, e.g.
{
  "booking-create": {
    "lambda\_memory\_size": 1024,
    "lambda\_timeout": 300
  }
}
|
map(object({
lambda_memory_size = optional(number),
lambda_timeout = optional(number),
schedule_expression = optional(string),
}))
| `{}` | no | 129 | | [parameter\_store\_list](#input\_parameter\_store\_list) | The list of parameter store keys to be used for the service, e.g.
[
  "datadog-api-key",
  "datadog-app-key",
  "sentry-dsn",
  "sentry-environment"
]
| `list(string)` | `[]` | no | 130 | | [service\_domain](#input\_service\_domain) | The 1st level of logical grouping of the service, e.g. 'api', 'web', 'db', etc. | `string` | n/a | yes | 131 | | [service\_environment](#input\_service\_environment) | The 3rd level of logical grouping of the service, e.g. 'dev', 'test', 'prod', etc. | `string` | n/a | yes | 132 | | [service\_name](#input\_service\_name) | The 2nd level of logical grouping of the service, e.g. 'my-api', 'my-web', 'my-db', etc. | `string` | n/a | yes | 133 | | [service\_version](#input\_service\_version) | The version of the service | `string` | `"v1.0.0"` | no | 134 | 135 | ## Outputs 136 | 137 | | Name | Description | 138 | |------|-------------| 139 | | [dynamodb-table-list](#output\_dynamodb-table-list) | List of DynamoDB Tables created | 140 | | [kms-alias](#output\_kms-alias) | KMS Alias created | 141 | | [kms-key](#output\_kms-key) | KMS Key created | 142 | | [lambda-function-list](#output\_lambda-function-list) | List of Lambda Functions created | 143 | | [lambda-function-role](#output\_lambda-function-role) | Lambda Function Role created | 144 | | [lambda-layer](#output\_lambda-layer) | Lambda Layer created | 145 | | [ssm-parameter-list](#output\_ssm-parameter-list) | List of SSM Parameters created | 146 | 147 | 148 | ## How to Setup 149 | To setup the example you can follow the following steps: 150 | * Copy the `terraform.tfvars.example` file to `terraform.tfvars` 151 | * Run `terraform init` 152 | * Run `terraform apply` 153 | -------------------------------------------------------------------------------- /data.tf: -------------------------------------------------------------------------------- 1 | # get current region of the aws account 2 | data "aws_region" "current" {} 3 | 4 | # get current caller identity of the aws account 5 | data "aws_caller_identity" "current" {} 6 | 7 | # create an archive of sources/src 8 | data "archive_file" "typescript-source" { 9 | type = "zip" 10 | source_dir = "${path.module}/sources/src" 11 | output_path = "${local.temporary_build_prefix}/typescript-source.zip" 12 | } 13 | 14 | # create an archive of sources/dist 15 | data "archive_file" "lambda-function-source" { 16 | type = "zip" 17 | depends_on = [null_resource.lambda-layer-source-builder] 18 | source_dir = "${path.module}/sources/dist" 19 | output_path = "${local.temporary_build_prefix}/lambda-function-source.zip" 20 | } 21 | 22 | # create an archive of local.temporary_build_prefix/lambda-layer-source 23 | data "archive_file" "lambda-layer-source" { 24 | type = "zip" 25 | depends_on = [null_resource.lambda-layer-source-builder] 26 | source_dir = "${local.temporary_build_prefix}/lambda-layer-source" 27 | output_path = "${local.temporary_build_prefix}/lambda-layer-source-${filemd5("${path.module}/sources/package-lock.json")}.zip" 28 | } 29 | 30 | # create iam policy for the service lambda function 31 | data "aws_iam_policy_document" "function-policy" { 32 | 33 | statement { 34 | sid = "SSMParameterStoreAccess" 35 | effect = "Allow" 36 | actions = [ 37 | "ssm:GetParametersByPath", 38 | ] 39 | resources = [ 40 | "arn:aws:ssm:${data.aws_region.current.id}:${data.aws_caller_identity.current.id}:parameter/service/${var.service_domain}/${var.service_name}/${var.service_environment}/*", 41 | ] 42 | } 43 | 44 | statement { 45 | sid = "DynamoDBTableAccess" 46 | effect = "Allow" 47 | actions = [ 48 | "dynamodb:Get*", 49 | "dynamodb:Put*", 50 | "dynamodb:Query", 51 | "dynamodb:Scan", 52 | "dynamodb:BatchWriteItem", 53 | "dynamodb:DescribeTable" 54 | ] 55 | resources = [ 56 | for table in var.dynamodb_table_list : "arn:aws:dynamodb:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:table/${module.dynamodb-table-name[table.name].name}" 57 | ] 58 | } 59 | 60 | statement { 61 | sid = "KMSKeyAccess" 62 | effect = "Allow" 63 | actions = [ 64 | "kms:Decrypt*", 65 | "kms:Encrypt*", 66 | ] 67 | resources = [ 68 | aws_kms_key.service-key.arn, 69 | aws_kms_alias.service-alias.arn, 70 | ] 71 | } 72 | 73 | statement { 74 | sid = "CloudWatchLogGroupAccess" 75 | effect = "Allow" 76 | actions = [ 77 | "logs:CreateLogGroup", 78 | "logs:CreateLogStream", 79 | "logs:PutLogEvents" 80 | ] 81 | resources = [ 82 | "arn:aws:logs:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:*", 83 | "arn:aws:logs:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/*:*", 84 | ] 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | 3 | # lambda runtime 4 | lambda_runtime = "nodejs14.x" 5 | 6 | # this is will generate the list of function name based on files residing in the functions directory 7 | # this is also parse the name of the function and generate the function name 8 | functions = toset([ 9 | for file in fileset("${path.module}/sources/src/functions", "*.function.ts") : lower(replace(split(".", split("/", file)[length(split("/", file)) - 1])[0], "[^a-zA-Z0-9]", "-")) 10 | ]) 11 | 12 | # temporary path prefix to store the generated files 13 | temporary_build_prefix = "/tmp/${data.aws_caller_identity.current.id}/${data.aws_region.current.id}/${var.service_name}" 14 | 15 | # define the default tags for the resources 16 | default_tags = merge({ 17 | "Name" : "${var.service_domain}-${var.service_name}-${var.service_environment}", 18 | "Environment" : "${var.service_environment}", 19 | "Service" : "${var.service_name}", 20 | "ServiceName" : "${var.service_name}", 21 | "ServiceDomain" : "${var.service_domain}", 22 | "ServiceVersion" : "${var.service_version}", 23 | }, var.default_tags) 24 | 25 | # define resources prefix name 26 | resource_prefix_name = substr(var.service_name, 0, length(var.service_domain)) == var.service_domain ? "${var.service_name}-${var.service_environment}" : "${var.service_domain}-${var.service_name}-${var.service_environment}" 27 | 28 | } 29 | 30 | # naming section to standardize the naming of the resources 31 | # naming for Lambda Layer 32 | module "lambda-layer-name" { 33 | source = "git@github.com:traveloka/terraform-aws-resource-naming.git?ref=v0.22.0" 34 | name_prefix = "${local.resource_prefix_name}-layer" 35 | resource_type = "lambda_function" 36 | } 37 | 38 | # naming for Lambda Function 39 | module "lambda-function-name" { 40 | for_each = local.functions 41 | source = "git@github.com:traveloka/terraform-aws-resource-naming.git?ref=v0.22.0" 42 | name_prefix = "${local.resource_prefix_name}-${each.value}" 43 | resource_type = "lambda_function" 44 | } 45 | 46 | # naming for DynamoDB Table 47 | module "dynamodb-table-name" { 48 | for_each = { for table in var.dynamodb_table_list : table.name => table } 49 | source = "git@github.com:traveloka/terraform-aws-resource-naming.git?ref=v0.22.0" 50 | name_prefix = "${local.resource_prefix_name}-${each.value.name}" 51 | resource_type = "dynamodb_table" 52 | } 53 | 54 | # build a TypeScript model based on DynamoDB table list 55 | # this will generate a TypeScript model for each DynamoDB table 56 | # and store it in the sources/src/models directory with format of .module.ts 57 | # the TypeScript model will only be generated if the model file does not exist 58 | resource "null_resource" "typescript-source-model-builder" { 59 | for_each = { for table in var.dynamodb_table_list : table.name => table } 60 | depends_on = [null_resource.lambda-layer-source-builder] 61 | provisioner "local-exec" { 62 | working_dir = "${path.module}/sources" 63 | command = < ${path.module}/src/models/${each.value.name}.model.ts 67 | echo "" >> ${path.module}/src/models/${each.value.name}.model.ts 68 | echo "export class ${join("", [for name_component in split("-", each.value.name) : "${upper(substr(name_component, 0, 1))}${substr(name_component, 1, length(name_component))}"])}Model extends Model {" >> ${path.module}/src/models/${each.value.name}.model.ts 69 | echo " public ${each.value.key}: string;" >> ${path.module}/src/models/${each.value.name}.model.ts 70 | echo " // insert your model properties here" >> ${path.module}/src/models/${each.value.name}.model.ts 71 | echo " // e.g. public name: string;" >> ${path.module}/src/models/${each.value.name}.model.ts 72 | echo "}" >> ${path.module}/src/models/${each.value.name}.model.ts 73 | fi 74 | EOT 75 | } 76 | triggers = { 77 | timestamp = timestamp() 78 | } 79 | } 80 | 81 | # build the dist directory using npm run build 82 | # this will generate the Javascript from Typescript source code using TypeScript compiler 83 | # this builder will only run when: 84 | # - there's a change in the source code directory 85 | # - there's a change in service domain value 86 | # - there's a change in service name value 87 | # - there's no existing dist directory 88 | resource "null_resource" "lambda-function-source-builder" { 89 | depends_on = [null_resource.typescript-source-model-builder] 90 | provisioner "local-exec" { 91 | working_dir = "${path.module}/sources" 92 | command = "npm run build" 93 | } 94 | triggers = { 95 | lambda-function-md5 = data.archive_file.typescript-source.output_md5 96 | service_domain = var.service_domain 97 | service_name = var.service_name 98 | file_dist = fileexists("${path.module}/sources/dist/index.js") ? "${path.module}/sources/dist/index.js" : timestamp() 99 | } 100 | } 101 | 102 | # build the dependencies directory using npm install 103 | # this will generate the dependencies file from the package.json file 104 | # this will be used as Lambda Layer archive file 105 | # this builder will only run when: 106 | # - there's a change in package.json file 107 | # - there's a change in package-lock.json file 108 | # - there's a change in service domain value 109 | # - there's a change in service name value 110 | # - there's no existing dependencies directory 111 | resource "null_resource" "lambda-layer-source-builder" { 112 | provisioner "local-exec" { 113 | working_dir = "${path.module}/sources" 114 | command = < table } 209 | 210 | name = module.dynamodb-table-name[each.value.name].name 211 | billing_mode = "PAY_PER_REQUEST" 212 | hash_key = each.value.key 213 | range_key = each.value.range_key != null ? each.value.range_key : null 214 | tags = local.default_tags 215 | 216 | point_in_time_recovery { 217 | enabled = true 218 | } 219 | 220 | server_side_encryption { 221 | enabled = true 222 | kms_key_arn = aws_kms_key.service-key.arn 223 | } 224 | 225 | attribute { 226 | name = each.value.key 227 | type = "S" 228 | } 229 | 230 | dynamic "attribute" { 231 | for_each = each.value.range_key != null ? [each.value.range_key] : [] 232 | content { 233 | name = each.value.range_key 234 | type = "S" 235 | } 236 | } 237 | 238 | } 239 | 240 | # create SSM Parameter Store to store DynamoDB table name based on the var.dynamodb_table_list value 241 | # this parameter will be used to load the environment variables to get the actual DynamoDB table name 242 | # the environment variables will be create with this format DYNAMODB_TABLE_ 243 | resource "aws_ssm_parameter" "ssm-parameter-dynamodb-table" { 244 | for_each = { for table in var.dynamodb_table_list : table.name => table } 245 | name = "/service/${var.service_domain}/${var.service_name}/${var.service_environment}/dynamodb-table-${each.value.name}" 246 | key_id = aws_kms_alias.service-alias.id 247 | type = "SecureString" 248 | value = module.dynamodb-table-name[each.value.name].name 249 | tags = local.default_tags 250 | } 251 | 252 | # create SSM Parameter Store based on the var.ssm_parameter_list value 253 | # this parameter will be used to load the environment variables to get the actual SSM parameter value 254 | resource "aws_ssm_parameter" "ssm-parameter-custom" { 255 | for_each = toset(var.parameter_store_list) 256 | name = "/service/${var.service_domain}/${var.service_name}/${var.service_environment}/${each.value}" 257 | key_id = aws_kms_alias.service-alias.id 258 | type = "SecureString" 259 | value = "placeholder" 260 | tags = local.default_tags 261 | lifecycle { 262 | ignore_changes = [value] 263 | } 264 | } 265 | 266 | # create SSM Parameter Store to store the service version 267 | # this parameter will be used to load the environment variables to get the actual service version 268 | # the environment variables will be create with this format SERVICE_VERSION 269 | resource "aws_ssm_parameter" "ssm-parameter-service-version" { 270 | name = "/service/${var.service_domain}/${var.service_name}/${var.service_environment}/service-version" 271 | key_id = aws_kms_alias.service-alias.id 272 | type = "String" 273 | value = var.service_version 274 | tags = local.default_tags 275 | } 276 | 277 | # attach the Lambda Function policy to the Lambda Function Role 278 | resource "aws_iam_role_policy" "function-policy" { 279 | for_each = aws_iam_role.lambda-function-role 280 | name = "function-policy" 281 | role = each.value.name 282 | policy = data.aws_iam_policy_document.function-policy.json 283 | } 284 | 285 | # create the Cloudwatch Event Rule for the Lambda Function with schedule_expression attribute 286 | resource "aws_cloudwatch_event_rule" "lambda-function-trigger-schedule" { 287 | for_each = { 288 | for name, configuration in var.lambda_function_configuration : name => configuration 289 | if configuration.schedule_expression != "" 290 | } 291 | description = "Lambda Function trigger schedule for ${each.key}" 292 | event_bus_name = "default" 293 | is_enabled = true 294 | name = each.key 295 | schedule_expression = each.value.schedule_expression 296 | tags = local.default_tags 297 | } 298 | 299 | # set the Cloudwatch Event Rule target to the Lambda Function 300 | resource "aws_cloudwatch_event_target" "lambda-function-trigger-schedule" { 301 | for_each = aws_cloudwatch_event_rule.lambda-function-trigger-schedule 302 | arn = aws_lambda_function.lambda-function[each.key].arn 303 | event_bus_name = "default" 304 | rule = aws_cloudwatch_event_rule.lambda-function-trigger-schedule[each.key].name 305 | target_id = "lambda-function-trigger-schedule-${each.key}" 306 | } 307 | 308 | # give the permission to the Cloudwatch Event Rule to invoke the Lambda Function 309 | resource "aws_lambda_permission" "lambda-function-trigger-schedule" { 310 | depends_on = [aws_lambda_function.lambda-function] 311 | for_each = aws_cloudwatch_event_rule.lambda-function-trigger-schedule 312 | statement_id = "AllowLambdaFunctionTriggerSchedule" 313 | action = "lambda:InvokeFunction" 314 | function_name = aws_lambda_function.lambda-function[each.key].function_name 315 | principal = "events.amazonaws.com" 316 | source_arn = aws_cloudwatch_event_rule.lambda-function-trigger-schedule[each.key].arn 317 | } 318 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "lambda-function-list" { 2 | value = aws_lambda_function.lambda-function 3 | description = "List of Lambda Functions created" 4 | } 5 | 6 | output "dynamodb-table-list" { 7 | value = aws_dynamodb_table.dynamodb-table 8 | description = "List of DynamoDB Tables created" 9 | } 10 | 11 | output "ssm-parameter-list" { 12 | value = merge( 13 | aws_ssm_parameter.ssm-parameter-dynamodb-table, 14 | aws_ssm_parameter.ssm-parameter-custom, 15 | aws_ssm_parameter.ssm-parameter-service-version 16 | ) 17 | sensitive = true 18 | description = "List of SSM Parameters created" 19 | } 20 | 21 | output "kms-key" { 22 | value = aws_kms_key.service-key 23 | description = "KMS Key created" 24 | } 25 | 26 | output "kms-alias" { 27 | value = aws_kms_alias.service-alias 28 | description = "KMS Alias created" 29 | } 30 | 31 | output "lambda-function-role" { 32 | value = aws_iam_role.lambda-function-role 33 | description = "Lambda Function Role created" 34 | } 35 | 36 | output "lambda-layer" { 37 | value = aws_lambda_layer_version.lambda-layer 38 | description = "Lambda Layer created" 39 | } 40 | -------------------------------------------------------------------------------- /providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | 3 | required_version = "~> 1.1.9" 4 | experiments = [module_variable_optional_attrs] 5 | 6 | required_providers { 7 | aws = { 8 | source = "hashicorp/aws" 9 | version = "~> 4.10.0" 10 | } 11 | } 12 | 13 | } 14 | 15 | provider "aws" { 16 | region = "ap-southeast-1" 17 | } 18 | -------------------------------------------------------------------------------- /sources/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /sources/.node-version: -------------------------------------------------------------------------------- 1 | 14.19.1 -------------------------------------------------------------------------------- /sources/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boilerplate-lambda-typescript", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@aws-crypto/ie11-detection": { 8 | "version": "2.0.0", 9 | "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.0.tgz", 10 | "integrity": "sha512-pkVXf/dq6PITJ0jzYZ69VhL8VFOFoPZLZqtU/12SGnzYuJOOGNfF41q9GxdI1yqC8R13Rq3jOLKDFpUJFT5eTA==", 11 | "dev": true, 12 | "requires": { 13 | "tslib": "^1.11.1" 14 | }, 15 | "dependencies": { 16 | "tslib": { 17 | "version": "1.14.1", 18 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 19 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", 20 | "dev": true 21 | } 22 | } 23 | }, 24 | "@aws-crypto/sha256-browser": { 25 | "version": "2.0.0", 26 | "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-2.0.0.tgz", 27 | "integrity": "sha512-rYXOQ8BFOaqMEHJrLHul/25ckWH6GTJtdLSajhlqGMx0PmSueAuvboCuZCTqEKlxR8CQOwRarxYMZZSYlhRA1A==", 28 | "dev": true, 29 | "requires": { 30 | "@aws-crypto/ie11-detection": "^2.0.0", 31 | "@aws-crypto/sha256-js": "^2.0.0", 32 | "@aws-crypto/supports-web-crypto": "^2.0.0", 33 | "@aws-crypto/util": "^2.0.0", 34 | "@aws-sdk/types": "^3.1.0", 35 | "@aws-sdk/util-locate-window": "^3.0.0", 36 | "@aws-sdk/util-utf8-browser": "^3.0.0", 37 | "tslib": "^1.11.1" 38 | }, 39 | "dependencies": { 40 | "tslib": { 41 | "version": "1.14.1", 42 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 43 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", 44 | "dev": true 45 | } 46 | } 47 | }, 48 | "@aws-crypto/sha256-js": { 49 | "version": "2.0.0", 50 | "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-2.0.0.tgz", 51 | "integrity": "sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig==", 52 | "dev": true, 53 | "requires": { 54 | "@aws-crypto/util": "^2.0.0", 55 | "@aws-sdk/types": "^3.1.0", 56 | "tslib": "^1.11.1" 57 | }, 58 | "dependencies": { 59 | "tslib": { 60 | "version": "1.14.1", 61 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 62 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", 63 | "dev": true 64 | } 65 | } 66 | }, 67 | "@aws-crypto/supports-web-crypto": { 68 | "version": "2.0.0", 69 | "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.0.tgz", 70 | "integrity": "sha512-Ge7WQ3E0OC7FHYprsZV3h0QIcpdyJLvIeg+uTuHqRYm8D6qCFJoiC+edSzSyFiHtZf+NOQDJ1q46qxjtzIY2nA==", 71 | "dev": true, 72 | "requires": { 73 | "tslib": "^1.11.1" 74 | }, 75 | "dependencies": { 76 | "tslib": { 77 | "version": "1.14.1", 78 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 79 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", 80 | "dev": true 81 | } 82 | } 83 | }, 84 | "@aws-crypto/util": { 85 | "version": "2.0.1", 86 | "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-2.0.1.tgz", 87 | "integrity": "sha512-JJmFFwvbm08lULw4Nm5QOLg8+lAQeC8aCXK5xrtxntYzYXCGfHwUJ4Is3770Q7HmICsXthGQ+ZsDL7C2uH3yBQ==", 88 | "dev": true, 89 | "requires": { 90 | "@aws-sdk/types": "^3.1.0", 91 | "@aws-sdk/util-utf8-browser": "^3.0.0", 92 | "tslib": "^1.11.1" 93 | }, 94 | "dependencies": { 95 | "tslib": { 96 | "version": "1.14.1", 97 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 98 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", 99 | "dev": true 100 | } 101 | } 102 | }, 103 | "@aws-sdk/abort-controller": { 104 | "version": "3.55.0", 105 | "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.55.0.tgz", 106 | "integrity": "sha512-rCcTxJDEFnmvo/PgbhCRv24/Uv03lEGfRslKZq7SjaMcOubflS/ZXYaMEgsjYHgAT0zlpSsyCIkJXmhFaM7H7w==", 107 | "dev": true, 108 | "requires": { 109 | "@aws-sdk/types": "3.55.0", 110 | "tslib": "^2.3.1" 111 | } 112 | }, 113 | "@aws-sdk/client-dynamodb": { 114 | "version": "3.76.0", 115 | "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.76.0.tgz", 116 | "integrity": "sha512-U8Jfn6hi7r76EtZDJbzVOpySXvYKxk8RoMdXdebworwvTuLbFfgefahY2A5BUY7ByMsgOcD5WwPkJURY9y5VYg==", 117 | "dev": true, 118 | "requires": { 119 | "@aws-crypto/sha256-browser": "2.0.0", 120 | "@aws-crypto/sha256-js": "2.0.0", 121 | "@aws-sdk/client-sts": "3.76.0", 122 | "@aws-sdk/config-resolver": "3.75.0", 123 | "@aws-sdk/credential-provider-node": "3.76.0", 124 | "@aws-sdk/fetch-http-handler": "3.58.0", 125 | "@aws-sdk/hash-node": "3.55.0", 126 | "@aws-sdk/invalid-dependency": "3.55.0", 127 | "@aws-sdk/middleware-content-length": "3.58.0", 128 | "@aws-sdk/middleware-endpoint-discovery": "3.75.0", 129 | "@aws-sdk/middleware-host-header": "3.58.0", 130 | "@aws-sdk/middleware-logger": "3.55.0", 131 | "@aws-sdk/middleware-retry": "3.75.0", 132 | "@aws-sdk/middleware-serde": "3.55.0", 133 | "@aws-sdk/middleware-signing": "3.58.0", 134 | "@aws-sdk/middleware-stack": "3.55.0", 135 | "@aws-sdk/middleware-user-agent": "3.58.0", 136 | "@aws-sdk/node-config-provider": "3.75.0", 137 | "@aws-sdk/node-http-handler": "3.76.0", 138 | "@aws-sdk/protocol-http": "3.58.0", 139 | "@aws-sdk/smithy-client": "3.72.0", 140 | "@aws-sdk/types": "3.55.0", 141 | "@aws-sdk/url-parser": "3.55.0", 142 | "@aws-sdk/util-base64-browser": "3.58.0", 143 | "@aws-sdk/util-base64-node": "3.55.0", 144 | "@aws-sdk/util-body-length-browser": "3.55.0", 145 | "@aws-sdk/util-body-length-node": "3.55.0", 146 | "@aws-sdk/util-defaults-mode-browser": "3.72.0", 147 | "@aws-sdk/util-defaults-mode-node": "3.75.0", 148 | "@aws-sdk/util-user-agent-browser": "3.58.0", 149 | "@aws-sdk/util-user-agent-node": "3.75.0", 150 | "@aws-sdk/util-utf8-browser": "3.55.0", 151 | "@aws-sdk/util-utf8-node": "3.55.0", 152 | "@aws-sdk/util-waiter": "3.55.0", 153 | "tslib": "^2.3.1", 154 | "uuid": "^8.3.2" 155 | } 156 | }, 157 | "@aws-sdk/client-ssm": { 158 | "version": "3.76.0", 159 | "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.76.0.tgz", 160 | "integrity": "sha512-V8DroRS035FWBjWLEuNcDOdJlPj/z3ytK0mO3b0BWNCt7pflCjaVFl5DSPEBKII21CphJuXAsICaDMxr4VL8TA==", 161 | "dev": true, 162 | "requires": { 163 | "@aws-crypto/sha256-browser": "2.0.0", 164 | "@aws-crypto/sha256-js": "2.0.0", 165 | "@aws-sdk/client-sts": "3.76.0", 166 | "@aws-sdk/config-resolver": "3.75.0", 167 | "@aws-sdk/credential-provider-node": "3.76.0", 168 | "@aws-sdk/fetch-http-handler": "3.58.0", 169 | "@aws-sdk/hash-node": "3.55.0", 170 | "@aws-sdk/invalid-dependency": "3.55.0", 171 | "@aws-sdk/middleware-content-length": "3.58.0", 172 | "@aws-sdk/middleware-host-header": "3.58.0", 173 | "@aws-sdk/middleware-logger": "3.55.0", 174 | "@aws-sdk/middleware-retry": "3.75.0", 175 | "@aws-sdk/middleware-serde": "3.55.0", 176 | "@aws-sdk/middleware-signing": "3.58.0", 177 | "@aws-sdk/middleware-stack": "3.55.0", 178 | "@aws-sdk/middleware-user-agent": "3.58.0", 179 | "@aws-sdk/node-config-provider": "3.75.0", 180 | "@aws-sdk/node-http-handler": "3.76.0", 181 | "@aws-sdk/protocol-http": "3.58.0", 182 | "@aws-sdk/smithy-client": "3.72.0", 183 | "@aws-sdk/types": "3.55.0", 184 | "@aws-sdk/url-parser": "3.55.0", 185 | "@aws-sdk/util-base64-browser": "3.58.0", 186 | "@aws-sdk/util-base64-node": "3.55.0", 187 | "@aws-sdk/util-body-length-browser": "3.55.0", 188 | "@aws-sdk/util-body-length-node": "3.55.0", 189 | "@aws-sdk/util-defaults-mode-browser": "3.72.0", 190 | "@aws-sdk/util-defaults-mode-node": "3.75.0", 191 | "@aws-sdk/util-user-agent-browser": "3.58.0", 192 | "@aws-sdk/util-user-agent-node": "3.75.0", 193 | "@aws-sdk/util-utf8-browser": "3.55.0", 194 | "@aws-sdk/util-utf8-node": "3.55.0", 195 | "@aws-sdk/util-waiter": "3.55.0", 196 | "tslib": "^2.3.1", 197 | "uuid": "^8.3.2" 198 | } 199 | }, 200 | "@aws-sdk/client-sso": { 201 | "version": "3.76.0", 202 | "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.76.0.tgz", 203 | "integrity": "sha512-trwzJWGxeagYAzo+1/JgcU/pM1vpKHW5rkbasDO5ZC4zHAlSwVhlU7yxGjYXsnobjkvf7zqTQhAxmOuMNWMFew==", 204 | "dev": true, 205 | "requires": { 206 | "@aws-crypto/sha256-browser": "2.0.0", 207 | "@aws-crypto/sha256-js": "2.0.0", 208 | "@aws-sdk/config-resolver": "3.75.0", 209 | "@aws-sdk/fetch-http-handler": "3.58.0", 210 | "@aws-sdk/hash-node": "3.55.0", 211 | "@aws-sdk/invalid-dependency": "3.55.0", 212 | "@aws-sdk/middleware-content-length": "3.58.0", 213 | "@aws-sdk/middleware-host-header": "3.58.0", 214 | "@aws-sdk/middleware-logger": "3.55.0", 215 | "@aws-sdk/middleware-retry": "3.75.0", 216 | "@aws-sdk/middleware-serde": "3.55.0", 217 | "@aws-sdk/middleware-stack": "3.55.0", 218 | "@aws-sdk/middleware-user-agent": "3.58.0", 219 | "@aws-sdk/node-config-provider": "3.75.0", 220 | "@aws-sdk/node-http-handler": "3.76.0", 221 | "@aws-sdk/protocol-http": "3.58.0", 222 | "@aws-sdk/smithy-client": "3.72.0", 223 | "@aws-sdk/types": "3.55.0", 224 | "@aws-sdk/url-parser": "3.55.0", 225 | "@aws-sdk/util-base64-browser": "3.58.0", 226 | "@aws-sdk/util-base64-node": "3.55.0", 227 | "@aws-sdk/util-body-length-browser": "3.55.0", 228 | "@aws-sdk/util-body-length-node": "3.55.0", 229 | "@aws-sdk/util-defaults-mode-browser": "3.72.0", 230 | "@aws-sdk/util-defaults-mode-node": "3.75.0", 231 | "@aws-sdk/util-user-agent-browser": "3.58.0", 232 | "@aws-sdk/util-user-agent-node": "3.75.0", 233 | "@aws-sdk/util-utf8-browser": "3.55.0", 234 | "@aws-sdk/util-utf8-node": "3.55.0", 235 | "tslib": "^2.3.1" 236 | } 237 | }, 238 | "@aws-sdk/client-sts": { 239 | "version": "3.76.0", 240 | "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.76.0.tgz", 241 | "integrity": "sha512-rrzau4y7VO9q/F6ZRuJAdZV5oKggjgJuUKGSGssYkLgO2BDblcR1ObUNetSyFsGPoSWnDhg0TjFJnlFFlIBplA==", 242 | "dev": true, 243 | "requires": { 244 | "@aws-crypto/sha256-browser": "2.0.0", 245 | "@aws-crypto/sha256-js": "2.0.0", 246 | "@aws-sdk/config-resolver": "3.75.0", 247 | "@aws-sdk/credential-provider-node": "3.76.0", 248 | "@aws-sdk/fetch-http-handler": "3.58.0", 249 | "@aws-sdk/hash-node": "3.55.0", 250 | "@aws-sdk/invalid-dependency": "3.55.0", 251 | "@aws-sdk/middleware-content-length": "3.58.0", 252 | "@aws-sdk/middleware-host-header": "3.58.0", 253 | "@aws-sdk/middleware-logger": "3.55.0", 254 | "@aws-sdk/middleware-retry": "3.75.0", 255 | "@aws-sdk/middleware-sdk-sts": "3.58.0", 256 | "@aws-sdk/middleware-serde": "3.55.0", 257 | "@aws-sdk/middleware-signing": "3.58.0", 258 | "@aws-sdk/middleware-stack": "3.55.0", 259 | "@aws-sdk/middleware-user-agent": "3.58.0", 260 | "@aws-sdk/node-config-provider": "3.75.0", 261 | "@aws-sdk/node-http-handler": "3.76.0", 262 | "@aws-sdk/protocol-http": "3.58.0", 263 | "@aws-sdk/smithy-client": "3.72.0", 264 | "@aws-sdk/types": "3.55.0", 265 | "@aws-sdk/url-parser": "3.55.0", 266 | "@aws-sdk/util-base64-browser": "3.58.0", 267 | "@aws-sdk/util-base64-node": "3.55.0", 268 | "@aws-sdk/util-body-length-browser": "3.55.0", 269 | "@aws-sdk/util-body-length-node": "3.55.0", 270 | "@aws-sdk/util-defaults-mode-browser": "3.72.0", 271 | "@aws-sdk/util-defaults-mode-node": "3.75.0", 272 | "@aws-sdk/util-user-agent-browser": "3.58.0", 273 | "@aws-sdk/util-user-agent-node": "3.75.0", 274 | "@aws-sdk/util-utf8-browser": "3.55.0", 275 | "@aws-sdk/util-utf8-node": "3.55.0", 276 | "entities": "2.2.0", 277 | "fast-xml-parser": "3.19.0", 278 | "tslib": "^2.3.1" 279 | } 280 | }, 281 | "@aws-sdk/config-resolver": { 282 | "version": "3.75.0", 283 | "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.75.0.tgz", 284 | "integrity": "sha512-sM1tygyXTEU8+UXAOs9353+lYoaWdtxPtxfC4zQsQUi0zUYCyO8jO7bNBo277uF82jkGwkraUL/F0ZN7KyzjSQ==", 285 | "dev": true, 286 | "requires": { 287 | "@aws-sdk/signature-v4": "3.58.0", 288 | "@aws-sdk/types": "3.55.0", 289 | "@aws-sdk/util-config-provider": "3.55.0", 290 | "@aws-sdk/util-middleware": "3.55.0", 291 | "tslib": "^2.3.1" 292 | } 293 | }, 294 | "@aws-sdk/credential-provider-env": { 295 | "version": "3.55.0", 296 | "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.55.0.tgz", 297 | "integrity": "sha512-4AIIXEdvinLlWNFtrUbUgoB7dkuV04RTcTruVWI4Ub4WSsuSCa72ZU1vqyvcEAOgGGLBmcSaGTWByjiD2sGcGA==", 298 | "dev": true, 299 | "requires": { 300 | "@aws-sdk/property-provider": "3.55.0", 301 | "@aws-sdk/types": "3.55.0", 302 | "tslib": "^2.3.1" 303 | } 304 | }, 305 | "@aws-sdk/credential-provider-imds": { 306 | "version": "3.75.0", 307 | "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.75.0.tgz", 308 | "integrity": "sha512-woqM/cZCnPvlel6t5o79CqT8doXe/7tSH5j8RPpfkYUwfdQwQqpjNqcO2QfkVzq4WsKfRZ92U00BhXsWDUZRfg==", 309 | "dev": true, 310 | "requires": { 311 | "@aws-sdk/node-config-provider": "3.75.0", 312 | "@aws-sdk/property-provider": "3.55.0", 313 | "@aws-sdk/types": "3.55.0", 314 | "@aws-sdk/url-parser": "3.55.0", 315 | "tslib": "^2.3.1" 316 | } 317 | }, 318 | "@aws-sdk/credential-provider-ini": { 319 | "version": "3.76.0", 320 | "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.76.0.tgz", 321 | "integrity": "sha512-2je7+yjAilgwB/jZwPnhW0P8McmuZoY29A9v45SZxRSW2yABuEUJ3EvcoieUXXNRRnEz96BrldpUHDC8VhXPJw==", 322 | "dev": true, 323 | "requires": { 324 | "@aws-sdk/credential-provider-env": "3.55.0", 325 | "@aws-sdk/credential-provider-imds": "3.75.0", 326 | "@aws-sdk/credential-provider-sso": "3.76.0", 327 | "@aws-sdk/credential-provider-web-identity": "3.55.0", 328 | "@aws-sdk/property-provider": "3.55.0", 329 | "@aws-sdk/shared-ini-file-loader": "3.75.0", 330 | "@aws-sdk/types": "3.55.0", 331 | "tslib": "^2.3.1" 332 | } 333 | }, 334 | "@aws-sdk/credential-provider-node": { 335 | "version": "3.76.0", 336 | "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.76.0.tgz", 337 | "integrity": "sha512-PCBB4sj/t5oatxuqogfB/TANMJWjE8zIAwJJagJdXgyo4vMZ8IsSjnkpMwXdUoyPq+rUx6zFq8XagJF+WW0PBw==", 338 | "dev": true, 339 | "requires": { 340 | "@aws-sdk/credential-provider-env": "3.55.0", 341 | "@aws-sdk/credential-provider-imds": "3.75.0", 342 | "@aws-sdk/credential-provider-ini": "3.76.0", 343 | "@aws-sdk/credential-provider-process": "3.75.0", 344 | "@aws-sdk/credential-provider-sso": "3.76.0", 345 | "@aws-sdk/credential-provider-web-identity": "3.55.0", 346 | "@aws-sdk/property-provider": "3.55.0", 347 | "@aws-sdk/shared-ini-file-loader": "3.75.0", 348 | "@aws-sdk/types": "3.55.0", 349 | "tslib": "^2.3.1" 350 | } 351 | }, 352 | "@aws-sdk/credential-provider-process": { 353 | "version": "3.75.0", 354 | "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.75.0.tgz", 355 | "integrity": "sha512-G5dvX37AvS+oLGpka2JXv9wS6uViYQnspJ/56RDmXQElE7ChHBRz89GB4lOOowVQMROzpP96LARr8XNJ4iFq/w==", 356 | "dev": true, 357 | "requires": { 358 | "@aws-sdk/property-provider": "3.55.0", 359 | "@aws-sdk/shared-ini-file-loader": "3.75.0", 360 | "@aws-sdk/types": "3.55.0", 361 | "tslib": "^2.3.1" 362 | } 363 | }, 364 | "@aws-sdk/credential-provider-sso": { 365 | "version": "3.76.0", 366 | "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.76.0.tgz", 367 | "integrity": "sha512-i2vD1nrq72dNOhfsNI2iRvmI+eaxZeXQCkE5WUqURT8nHCloEkKDPchWWY2obUCVAnL1EPEoSKHyAETl1uSYew==", 368 | "dev": true, 369 | "requires": { 370 | "@aws-sdk/client-sso": "3.76.0", 371 | "@aws-sdk/property-provider": "3.55.0", 372 | "@aws-sdk/shared-ini-file-loader": "3.75.0", 373 | "@aws-sdk/types": "3.55.0", 374 | "tslib": "^2.3.1" 375 | } 376 | }, 377 | "@aws-sdk/credential-provider-web-identity": { 378 | "version": "3.55.0", 379 | "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.55.0.tgz", 380 | "integrity": "sha512-aKnXfZNGohTuF9rCGYLg4JEIOvWIZ/sb66XMq7bOUrx13KRPDwL/eUQL8quS5jGRLpjXVNvrS17AFf65GbdUBg==", 381 | "dev": true, 382 | "requires": { 383 | "@aws-sdk/property-provider": "3.55.0", 384 | "@aws-sdk/types": "3.55.0", 385 | "tslib": "^2.3.1" 386 | } 387 | }, 388 | "@aws-sdk/endpoint-cache": { 389 | "version": "3.55.0", 390 | "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.55.0.tgz", 391 | "integrity": "sha512-kxDoHFDuQwZEEUZRp+ZLOg68EXuKPzUN86DcpIZantDVcmu7MSPTbbQp9DZd8MnKVEKCP7Sop5f7zCqOPl3LXw==", 392 | "dev": true, 393 | "requires": { 394 | "mnemonist": "0.38.3", 395 | "tslib": "^2.3.1" 396 | } 397 | }, 398 | "@aws-sdk/fetch-http-handler": { 399 | "version": "3.58.0", 400 | "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.58.0.tgz", 401 | "integrity": "sha512-timF3FjPV5Bd+Kgph83LIKVlPCFObVYzious1a6doeLAT6YFwZpRrWbfP/HzS+DCoYiwUsH69oVJ91BoV66oyA==", 402 | "dev": true, 403 | "requires": { 404 | "@aws-sdk/protocol-http": "3.58.0", 405 | "@aws-sdk/querystring-builder": "3.55.0", 406 | "@aws-sdk/types": "3.55.0", 407 | "@aws-sdk/util-base64-browser": "3.58.0", 408 | "tslib": "^2.3.1" 409 | } 410 | }, 411 | "@aws-sdk/hash-node": { 412 | "version": "3.55.0", 413 | "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.55.0.tgz", 414 | "integrity": "sha512-2UdYwY/++AlzWEAFaK9wOed2QSxbzV527vmqKjReLHpPKPrSIlooUxlTH3LU6Y6WVDAzDRtLK43KUVXTLgGK1A==", 415 | "dev": true, 416 | "requires": { 417 | "@aws-sdk/types": "3.55.0", 418 | "@aws-sdk/util-buffer-from": "3.55.0", 419 | "tslib": "^2.3.1" 420 | } 421 | }, 422 | "@aws-sdk/invalid-dependency": { 423 | "version": "3.55.0", 424 | "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.55.0.tgz", 425 | "integrity": "sha512-delH0lV+78fdD/8MXIt9kTLS6IwHvdhqq9dw/ow5VjTUw+xBwUlfPfZplaai+3hKTKWh6a2WZCeDasNItBv9aA==", 426 | "dev": true, 427 | "requires": { 428 | "@aws-sdk/types": "3.55.0", 429 | "tslib": "^2.3.1" 430 | } 431 | }, 432 | "@aws-sdk/is-array-buffer": { 433 | "version": "3.55.0", 434 | "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.55.0.tgz", 435 | "integrity": "sha512-NbiPHVYuPxdqdFd6FxzzN3H1BQn/iWA3ri3Ry7AyLeP/tGs1yzEWMwf8BN8TSMALI0GXT6Sh0GDWy3Ok5xB6DA==", 436 | "dev": true, 437 | "requires": { 438 | "tslib": "^2.3.1" 439 | } 440 | }, 441 | "@aws-sdk/lib-dynamodb": { 442 | "version": "3.76.0", 443 | "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.76.0.tgz", 444 | "integrity": "sha512-ok8uFiRir5NsZGChYzYkjT+SB59l0WBGns4iEmSD2rZ/yXpdC240nnOFQJv+DkYVVCA0ZlkGVyJeZ+AoVE6jcA==", 445 | "dev": true, 446 | "requires": { 447 | "@aws-sdk/util-dynamodb": "3.76.0", 448 | "tslib": "^2.3.1" 449 | } 450 | }, 451 | "@aws-sdk/middleware-content-length": { 452 | "version": "3.58.0", 453 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.58.0.tgz", 454 | "integrity": "sha512-h/BypPkhjv2CpCUbXA8Fa2s7V2GPiz9l11XhYK+sKSuQvQ7Lbq6VhaKaLqfeD3gLVZHgJZSLGl2btdHV1qHNNA==", 455 | "dev": true, 456 | "requires": { 457 | "@aws-sdk/protocol-http": "3.58.0", 458 | "@aws-sdk/types": "3.55.0", 459 | "tslib": "^2.3.1" 460 | } 461 | }, 462 | "@aws-sdk/middleware-endpoint-discovery": { 463 | "version": "3.75.0", 464 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.75.0.tgz", 465 | "integrity": "sha512-6dJUWlAy/j8fRh1vbROCdK9AA2B/jxMIqTe1zxuBw+sPdlMcMvsuZLb7fK9K+WdgU34pGbef5UTkWSGBK0DuIQ==", 466 | "dev": true, 467 | "requires": { 468 | "@aws-sdk/config-resolver": "3.75.0", 469 | "@aws-sdk/endpoint-cache": "3.55.0", 470 | "@aws-sdk/protocol-http": "3.58.0", 471 | "@aws-sdk/types": "3.55.0", 472 | "tslib": "^2.3.1" 473 | } 474 | }, 475 | "@aws-sdk/middleware-host-header": { 476 | "version": "3.58.0", 477 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.58.0.tgz", 478 | "integrity": "sha512-q/UKGcanm9e6DBRNN6UKhVqLvpRRdZWbmmPCeDNr4HqhCmgT6i1OvWdhAMOnT++hvCX8DpTsIXzNSlY6zWAxBg==", 479 | "dev": true, 480 | "requires": { 481 | "@aws-sdk/protocol-http": "3.58.0", 482 | "@aws-sdk/types": "3.55.0", 483 | "tslib": "^2.3.1" 484 | } 485 | }, 486 | "@aws-sdk/middleware-logger": { 487 | "version": "3.55.0", 488 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.55.0.tgz", 489 | "integrity": "sha512-PtRbVrxEzDmeV9prBIP4/9or7R5Dj66mjbFSvNRGZ0n+UBfBFfVRfNrhQPNzQpfV9A3KVl9YyWCVXDSW+/rk9Q==", 490 | "dev": true, 491 | "requires": { 492 | "@aws-sdk/types": "3.55.0", 493 | "tslib": "^2.3.1" 494 | } 495 | }, 496 | "@aws-sdk/middleware-retry": { 497 | "version": "3.75.0", 498 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.75.0.tgz", 499 | "integrity": "sha512-6aQqeasv31d3Iu9t5YyrbbG5m8VKvjTJ+Aeio976ImhZZEEHeh6Hl2i6yX1DvOALIZmFjjMFNHwJkNOVuxXrXg==", 500 | "dev": true, 501 | "requires": { 502 | "@aws-sdk/protocol-http": "3.58.0", 503 | "@aws-sdk/service-error-classification": "3.55.0", 504 | "@aws-sdk/types": "3.55.0", 505 | "@aws-sdk/util-middleware": "3.55.0", 506 | "tslib": "^2.3.1", 507 | "uuid": "^8.3.2" 508 | } 509 | }, 510 | "@aws-sdk/middleware-sdk-sts": { 511 | "version": "3.58.0", 512 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.58.0.tgz", 513 | "integrity": "sha512-HUz7MhcsSDDTGygOwL61l4voc0pZco06J3z06JjTX19D5XxcQ7hSCtkHHHz0oMb9M1himVSiEon2tjhjsnB99g==", 514 | "dev": true, 515 | "requires": { 516 | "@aws-sdk/middleware-signing": "3.58.0", 517 | "@aws-sdk/property-provider": "3.55.0", 518 | "@aws-sdk/protocol-http": "3.58.0", 519 | "@aws-sdk/signature-v4": "3.58.0", 520 | "@aws-sdk/types": "3.55.0", 521 | "tslib": "^2.3.1" 522 | } 523 | }, 524 | "@aws-sdk/middleware-serde": { 525 | "version": "3.55.0", 526 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.55.0.tgz", 527 | "integrity": "sha512-NkEbTDrSZcC2NhuvfjXHKJEl0xgI2B5tMAwi/rMOq/TEnARwVUL9qAy+5lgeiPCqebiNllWatARrFgAaYf0VeA==", 528 | "dev": true, 529 | "requires": { 530 | "@aws-sdk/types": "3.55.0", 531 | "tslib": "^2.3.1" 532 | } 533 | }, 534 | "@aws-sdk/middleware-signing": { 535 | "version": "3.58.0", 536 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.58.0.tgz", 537 | "integrity": "sha512-4FXubHB66GbhyZUlo6YPQoWpYfED15GNbEmHbJLSONzrVzZR3IkViSPLasDngVm1a050JqKuqNkFYGJBP4No/Q==", 538 | "dev": true, 539 | "requires": { 540 | "@aws-sdk/property-provider": "3.55.0", 541 | "@aws-sdk/protocol-http": "3.58.0", 542 | "@aws-sdk/signature-v4": "3.58.0", 543 | "@aws-sdk/types": "3.55.0", 544 | "tslib": "^2.3.1" 545 | } 546 | }, 547 | "@aws-sdk/middleware-stack": { 548 | "version": "3.55.0", 549 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.55.0.tgz", 550 | "integrity": "sha512-ouD+wFz8W2R0ZQ8HrbhgN8tg1jyINEg9lPEEXY79w1Q5sf94LJ90XKAMVk02rw3dJalUWjLHf0OQe1/qxZfHyA==", 551 | "dev": true, 552 | "requires": { 553 | "tslib": "^2.3.1" 554 | } 555 | }, 556 | "@aws-sdk/middleware-user-agent": { 557 | "version": "3.58.0", 558 | "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.58.0.tgz", 559 | "integrity": "sha512-1c69bIWM63JwXijXvb9IWwcwQ/gViKMZ1lhxv52NvdG5VSxWXXsFJ2jETEXZoAypwT97Hmf3xo9SYuaHcKoq+g==", 560 | "dev": true, 561 | "requires": { 562 | "@aws-sdk/protocol-http": "3.58.0", 563 | "@aws-sdk/types": "3.55.0", 564 | "tslib": "^2.3.1" 565 | } 566 | }, 567 | "@aws-sdk/node-config-provider": { 568 | "version": "3.75.0", 569 | "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.75.0.tgz", 570 | "integrity": "sha512-eSR0HtqBwRp71d7Cp9fWzC+jtM5sDBcnp4vIQDIBPnHVzvMFwo2YPG0eF5SoYUgboHasHW8VGx9dUsKJ/qTcOg==", 571 | "dev": true, 572 | "requires": { 573 | "@aws-sdk/property-provider": "3.55.0", 574 | "@aws-sdk/shared-ini-file-loader": "3.75.0", 575 | "@aws-sdk/types": "3.55.0", 576 | "tslib": "^2.3.1" 577 | } 578 | }, 579 | "@aws-sdk/node-http-handler": { 580 | "version": "3.76.0", 581 | "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.76.0.tgz", 582 | "integrity": "sha512-zPWPoaFC5n71efREtpSF1seijZ2E+Wsxz56EK3G55BY7WcSlLgdPXtOS1GXCFtq9Ce6gNALhYvaIryITrbtWsw==", 583 | "dev": true, 584 | "requires": { 585 | "@aws-sdk/abort-controller": "3.55.0", 586 | "@aws-sdk/protocol-http": "3.58.0", 587 | "@aws-sdk/querystring-builder": "3.55.0", 588 | "@aws-sdk/types": "3.55.0", 589 | "tslib": "^2.3.1" 590 | } 591 | }, 592 | "@aws-sdk/property-provider": { 593 | "version": "3.55.0", 594 | "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.55.0.tgz", 595 | "integrity": "sha512-o7cKFJSHq5WOhwPsspYrzNto35oKKZvESZuWDtLxaZKSI6l7zpA366BI4kDG6Tc9i2+teV553MbxyZ9eya5A8g==", 596 | "dev": true, 597 | "requires": { 598 | "@aws-sdk/types": "3.55.0", 599 | "tslib": "^2.3.1" 600 | } 601 | }, 602 | "@aws-sdk/protocol-http": { 603 | "version": "3.58.0", 604 | "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.58.0.tgz", 605 | "integrity": "sha512-0yFFRPbR+CCa9eOQBBQ2qtrIDLYqSMN0y7G4iqVM8wQdIw7n3QK1PsTI3RNPGJ3Oi2krFTw5uUKqQQZPZEBuVQ==", 606 | "dev": true, 607 | "requires": { 608 | "@aws-sdk/types": "3.55.0", 609 | "tslib": "^2.3.1" 610 | } 611 | }, 612 | "@aws-sdk/querystring-builder": { 613 | "version": "3.55.0", 614 | "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.55.0.tgz", 615 | "integrity": "sha512-/ZAXNipt9nRR8k+eowwukE/YjXnQ49p5w/MkaQxsBk3IuIf7MAcgVg8glHr0igH84GfUQ7ZVP8v+G2S3tKUG+Q==", 616 | "dev": true, 617 | "requires": { 618 | "@aws-sdk/types": "3.55.0", 619 | "@aws-sdk/util-uri-escape": "3.55.0", 620 | "tslib": "^2.3.1" 621 | } 622 | }, 623 | "@aws-sdk/querystring-parser": { 624 | "version": "3.55.0", 625 | "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.55.0.tgz", 626 | "integrity": "sha512-e+2FLgo+eDx7oh7ap5HngN9XSVMxredAVztLHxCcSN0lFHHHzMa8b2SpXbaowUxQHh7ziymSqvOrPYFQ71Filg==", 627 | "dev": true, 628 | "requires": { 629 | "@aws-sdk/types": "3.55.0", 630 | "tslib": "^2.3.1" 631 | } 632 | }, 633 | "@aws-sdk/service-error-classification": { 634 | "version": "3.55.0", 635 | "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.55.0.tgz", 636 | "integrity": "sha512-HdjnDyarsa1Avq1MJurkLyEe9c3eRa76dPmK4TmRGgwJ+tInEzGHL0rBW7V8xBK+PDF+fJQ71hvm8jPYmzvBwQ==", 637 | "dev": true 638 | }, 639 | "@aws-sdk/shared-ini-file-loader": { 640 | "version": "3.75.0", 641 | "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.75.0.tgz", 642 | "integrity": "sha512-xNeBKoEqBWTdlSNhd0oA0ToA915zvKuAYHppOqJlAHpXQhjZN+Jtz31Rlor/EKZbHSMmZX7YzYMHhYWtY8aeCA==", 643 | "dev": true, 644 | "requires": { 645 | "tslib": "^2.3.1" 646 | } 647 | }, 648 | "@aws-sdk/signature-v4": { 649 | "version": "3.58.0", 650 | "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.58.0.tgz", 651 | "integrity": "sha512-flEo8p3XkzWoBDqnIUQre4jLuT5aLnmfQNI8c2uSjyJ3OBxpJ0iS1cDu3E++d1/pN6Q8o0KOmr2ypHeiyBOujw==", 652 | "dev": true, 653 | "requires": { 654 | "@aws-sdk/is-array-buffer": "3.55.0", 655 | "@aws-sdk/types": "3.55.0", 656 | "@aws-sdk/util-hex-encoding": "3.58.0", 657 | "@aws-sdk/util-middleware": "3.55.0", 658 | "@aws-sdk/util-uri-escape": "3.55.0", 659 | "tslib": "^2.3.1" 660 | } 661 | }, 662 | "@aws-sdk/smithy-client": { 663 | "version": "3.72.0", 664 | "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.72.0.tgz", 665 | "integrity": "sha512-eQ2pEzxtS1Vz1XyNKzG4Z+mtfwRzcAs4FUQP0wrrYVJMsIdI0X4vvro8gYGoBbQtOz65uY3XqQdLuXX/SabTQg==", 666 | "dev": true, 667 | "requires": { 668 | "@aws-sdk/middleware-stack": "3.55.0", 669 | "@aws-sdk/types": "3.55.0", 670 | "tslib": "^2.3.1" 671 | } 672 | }, 673 | "@aws-sdk/types": { 674 | "version": "3.55.0", 675 | "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.55.0.tgz", 676 | "integrity": "sha512-wrDZjuy1CVAYxDCbm3bWQIKMGfNs7XXmG0eG4858Ixgqmq2avsIn5TORy8ynBxcXn9aekV/+tGEQ7BBSYzIVNQ==", 677 | "dev": true 678 | }, 679 | "@aws-sdk/url-parser": { 680 | "version": "3.55.0", 681 | "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.55.0.tgz", 682 | "integrity": "sha512-qrTwN5xIgTLreqLnZ+x3cAudjNKfxi6srW1H/px2mk4lb2U9B4fpGjZ6VU+XV8U2kR+YlT8J6Jo5iwuVGfC91A==", 683 | "dev": true, 684 | "requires": { 685 | "@aws-sdk/querystring-parser": "3.55.0", 686 | "@aws-sdk/types": "3.55.0", 687 | "tslib": "^2.3.1" 688 | } 689 | }, 690 | "@aws-sdk/util-base64-browser": { 691 | "version": "3.58.0", 692 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-browser/-/util-base64-browser-3.58.0.tgz", 693 | "integrity": "sha512-0ebsXIZNpu/fup9OgsFPnRKfCFbuuI9PPRzvP6twzLxUB0c/aix6Co7LGHFKcRKHZdaykoJMXArf8eHj2Nzv1Q==", 694 | "dev": true, 695 | "requires": { 696 | "tslib": "^2.3.1" 697 | } 698 | }, 699 | "@aws-sdk/util-base64-node": { 700 | "version": "3.55.0", 701 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-node/-/util-base64-node-3.55.0.tgz", 702 | "integrity": "sha512-UQ/ZuNoAc8CFMpSiRYmevaTsuRKzLwulZTnM8LNlIt9Wx1tpNvqp80cfvVj7yySKROtEi20wq29h31dZf1eYNQ==", 703 | "dev": true, 704 | "requires": { 705 | "@aws-sdk/util-buffer-from": "3.55.0", 706 | "tslib": "^2.3.1" 707 | } 708 | }, 709 | "@aws-sdk/util-body-length-browser": { 710 | "version": "3.55.0", 711 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.55.0.tgz", 712 | "integrity": "sha512-Ei2OCzXQw5N6ZkTMZbamUzc1z+z1R1Ja5tMEagz5BxuX4vWdBObT+uGlSzL8yvTbjoPjnxWA2aXyEqaUP3JS8Q==", 713 | "dev": true, 714 | "requires": { 715 | "tslib": "^2.3.1" 716 | } 717 | }, 718 | "@aws-sdk/util-body-length-node": { 719 | "version": "3.55.0", 720 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.55.0.tgz", 721 | "integrity": "sha512-lU1d4I+9wJwydduXs0SxSfd+mHKjxeyd39VwOv6i2KSwWkPbji9UQqpflKLKw+r45jL7+xU/zfeTUg5Tt/3Gew==", 722 | "dev": true, 723 | "requires": { 724 | "tslib": "^2.3.1" 725 | } 726 | }, 727 | "@aws-sdk/util-buffer-from": { 728 | "version": "3.55.0", 729 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.55.0.tgz", 730 | "integrity": "sha512-uVzKG1UgvnV7XX2FPTylBujYMKBPBaq/qFBxfl0LVNfrty7YjpfieQxAe6yRLD+T0Kir/WDQwGvYC+tOYG3IGA==", 731 | "dev": true, 732 | "requires": { 733 | "@aws-sdk/is-array-buffer": "3.55.0", 734 | "tslib": "^2.3.1" 735 | } 736 | }, 737 | "@aws-sdk/util-config-provider": { 738 | "version": "3.55.0", 739 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.55.0.tgz", 740 | "integrity": "sha512-30dzofQQfx6tp1jVZkZ0DGRsT0wwC15nEysKRiAcjncM64A0Cm6sra77d0os3vbKiKoPCI/lMsFr4o3533+qvQ==", 741 | "dev": true, 742 | "requires": { 743 | "tslib": "^2.3.1" 744 | } 745 | }, 746 | "@aws-sdk/util-defaults-mode-browser": { 747 | "version": "3.72.0", 748 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.72.0.tgz", 749 | "integrity": "sha512-xeoh4jdq+tpZWDwGeXeoAQI+rZaCBEicjumBcqfzkRFE3DyaeyPHn3hiKGSR13R+P6Uf86aqaRNmWAeZZjeE0w==", 750 | "dev": true, 751 | "requires": { 752 | "@aws-sdk/property-provider": "3.55.0", 753 | "@aws-sdk/types": "3.55.0", 754 | "bowser": "^2.11.0", 755 | "tslib": "^2.3.1" 756 | } 757 | }, 758 | "@aws-sdk/util-defaults-mode-node": { 759 | "version": "3.75.0", 760 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.75.0.tgz", 761 | "integrity": "sha512-zR53YinMCSVcdXumxBMdnZANl5ld0riuEoDwgKIivag/5xOAp/r+PziYvaMDbIvdqtkwwMBXf+WAc9jb0/D7sg==", 762 | "dev": true, 763 | "requires": { 764 | "@aws-sdk/config-resolver": "3.75.0", 765 | "@aws-sdk/credential-provider-imds": "3.75.0", 766 | "@aws-sdk/node-config-provider": "3.75.0", 767 | "@aws-sdk/property-provider": "3.55.0", 768 | "@aws-sdk/types": "3.55.0", 769 | "tslib": "^2.3.1" 770 | } 771 | }, 772 | "@aws-sdk/util-dynamodb": { 773 | "version": "3.76.0", 774 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.76.0.tgz", 775 | "integrity": "sha512-cFgfkCHE2C4iOHxb65plqzXyGaP3yjgATxrJvHMiG1vTj/KNoV+hkO/DGSEvr9Z67JlP9yAvHO2LmM048zSVEw==", 776 | "dev": true, 777 | "requires": { 778 | "tslib": "^2.3.1" 779 | } 780 | }, 781 | "@aws-sdk/util-hex-encoding": { 782 | "version": "3.58.0", 783 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.58.0.tgz", 784 | "integrity": "sha512-Rl+jXUzk/FJkOLYfUVYPhKa2aUmTpeobRP31l8IatQltSzDgLyRHO35f6UEs7Ztn5s1jbu/POatLAZ2WjbgVyg==", 785 | "dev": true, 786 | "requires": { 787 | "tslib": "^2.3.1" 788 | } 789 | }, 790 | "@aws-sdk/util-locate-window": { 791 | "version": "3.55.0", 792 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.55.0.tgz", 793 | "integrity": "sha512-0sPmK2JaJE2BbTcnvybzob/VrFKCXKfN4CUKcvn0yGg/me7Bz+vtzQRB3Xp+YSx+7OtWxzv63wsvHoAnXvgxgg==", 794 | "dev": true, 795 | "requires": { 796 | "tslib": "^2.3.1" 797 | } 798 | }, 799 | "@aws-sdk/util-middleware": { 800 | "version": "3.55.0", 801 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.55.0.tgz", 802 | "integrity": "sha512-82fW2XV+rUalv8lkd4VlhpPp6xnXO5n9sckMp1N+TrQ+p8eqxqT0+o8n1/6s9Qsnkw64Y3m6+EfCdc8/uFOY2g==", 803 | "dev": true, 804 | "requires": { 805 | "tslib": "^2.3.1" 806 | } 807 | }, 808 | "@aws-sdk/util-uri-escape": { 809 | "version": "3.55.0", 810 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.55.0.tgz", 811 | "integrity": "sha512-mmdDLUpFCN2nkfwlLdOM54lTD528GiGSPN1qb8XtGLgZsJUmg3uJSFIN2lPeSbEwJB3NFjVas/rnQC48i7mV8w==", 812 | "dev": true, 813 | "requires": { 814 | "tslib": "^2.3.1" 815 | } 816 | }, 817 | "@aws-sdk/util-user-agent-browser": { 818 | "version": "3.58.0", 819 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.58.0.tgz", 820 | "integrity": "sha512-aJpqCvT09giJRg5xFTBDBRAVF0k0yq3OEf6UTuiOVf5azlL2MGp6PJ/xkJp9Z06PuQQkwBJ/2nIQZemo02a5Sw==", 821 | "dev": true, 822 | "requires": { 823 | "@aws-sdk/types": "3.55.0", 824 | "bowser": "^2.11.0", 825 | "tslib": "^2.3.1" 826 | } 827 | }, 828 | "@aws-sdk/util-user-agent-node": { 829 | "version": "3.75.0", 830 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.75.0.tgz", 831 | "integrity": "sha512-tUKI/WIhPjGwIxFZIApWz64/JwJwwzt55Rxp8kv0cP/rYVjfCZafokUKLRwJaOBWi79luvNKV7V6lXY7RjT61A==", 832 | "dev": true, 833 | "requires": { 834 | "@aws-sdk/node-config-provider": "3.75.0", 835 | "@aws-sdk/types": "3.55.0", 836 | "tslib": "^2.3.1" 837 | } 838 | }, 839 | "@aws-sdk/util-utf8-browser": { 840 | "version": "3.55.0", 841 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.55.0.tgz", 842 | "integrity": "sha512-ljzqJcyjfJpEVSIAxwtIS8xMRUly84BdjlBXyp6cu4G8TUufgjNS31LWdhyGhgmW5vYBNr+LTz0Kwf6J+ou7Ug==", 843 | "dev": true, 844 | "requires": { 845 | "tslib": "^2.3.1" 846 | } 847 | }, 848 | "@aws-sdk/util-utf8-node": { 849 | "version": "3.55.0", 850 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.55.0.tgz", 851 | "integrity": "sha512-FsFm7GFaC7j0tlPEm/ri8bU2QCwFW5WKjxUg8lm1oWaxplCpKGUsmcfPJ4sw58GIoyoGu4QXBK60oCWosZYYdQ==", 852 | "dev": true, 853 | "requires": { 854 | "@aws-sdk/util-buffer-from": "3.55.0", 855 | "tslib": "^2.3.1" 856 | } 857 | }, 858 | "@aws-sdk/util-waiter": { 859 | "version": "3.55.0", 860 | "resolved": "https://registry.npmjs.org/@aws-sdk/util-waiter/-/util-waiter-3.55.0.tgz", 861 | "integrity": "sha512-Do34MKPFSC/+zVN6vY+FZ+0WN61hzga4nPoAC590AOjs8rW6/H6sDN6Gz1KAZbPnuQUZfvsIJjMxN7lblXHJkQ==", 862 | "dev": true, 863 | "requires": { 864 | "@aws-sdk/abort-controller": "3.55.0", 865 | "@aws-sdk/types": "3.55.0", 866 | "tslib": "^2.3.1" 867 | } 868 | }, 869 | "@datadog/datadog-api-client": { 870 | "version": "1.0.0-beta.9", 871 | "resolved": "https://registry.npmjs.org/@datadog/datadog-api-client/-/datadog-api-client-1.0.0-beta.9.tgz", 872 | "integrity": "sha512-yCjrLVZvuxpsZhfriyFZb3q7s0IY/aK7AmyovKwGa/7LZoaaQE8ry6C/9cmWFaUfqie5t3oWr50eRKVhe+Dm6A==", 873 | "requires": { 874 | "@types/buffer-from": "^1.1.0", 875 | "@types/node": "*", 876 | "@types/pako": "^1.0.3", 877 | "btoa": "^1.2.1", 878 | "buffer-from": "^1.1.2", 879 | "cross-fetch": "^3.1.5", 880 | "durations": "^3.4.2", 881 | "es6-promise": "^4.2.4", 882 | "form-data": "^3.0.0", 883 | "loglevel": "^1.7.1", 884 | "pako": "^2.0.4", 885 | "url-parse": "^1.4.3" 886 | } 887 | }, 888 | "@types/aws-lambda": { 889 | "version": "8.10.95", 890 | "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.95.tgz", 891 | "integrity": "sha512-wGtzLbd04EmqhFjTZmXgLzvmhDdyVU7AMo/JkiPmA2VUdBFQfUBQFCEzaVVK+f1PP5aWx1ejnb7K/8MXYI/frQ==", 892 | "dev": true 893 | }, 894 | "@types/buffer-from": { 895 | "version": "1.1.0", 896 | "resolved": "https://registry.npmjs.org/@types/buffer-from/-/buffer-from-1.1.0.tgz", 897 | "integrity": "sha512-BLFpLBcN+RPKUsFxqRkMiwqTOOdi+TrKr5OpLJ9qCnUdSxS6S80+QRX/mIhfR66u0Ykc4QTkReaejOM2ILh+9Q==", 898 | "requires": { 899 | "@types/node": "*" 900 | } 901 | }, 902 | "@types/node": { 903 | "version": "17.0.25", 904 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.25.tgz", 905 | "integrity": "sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==" 906 | }, 907 | "@types/pako": { 908 | "version": "1.0.3", 909 | "resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.3.tgz", 910 | "integrity": "sha512-EDxOsHAD5dqjbjEUM1xwa7rpKPFb8ECBE5irONTQU7/OsO3thI5YrNEWSPNMvYmvFM0l/OLQJ6Mgw7PEdXSjhg==" 911 | }, 912 | "asynckit": { 913 | "version": "0.4.0", 914 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 915 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 916 | }, 917 | "bowser": { 918 | "version": "2.11.0", 919 | "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", 920 | "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", 921 | "dev": true 922 | }, 923 | "btoa": { 924 | "version": "1.2.1", 925 | "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", 926 | "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" 927 | }, 928 | "buffer-from": { 929 | "version": "1.1.2", 930 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 931 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" 932 | }, 933 | "combined-stream": { 934 | "version": "1.0.8", 935 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 936 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 937 | "requires": { 938 | "delayed-stream": "~1.0.0" 939 | } 940 | }, 941 | "cross-fetch": { 942 | "version": "3.1.5", 943 | "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", 944 | "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", 945 | "requires": { 946 | "node-fetch": "2.6.7" 947 | } 948 | }, 949 | "delayed-stream": { 950 | "version": "1.0.0", 951 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 952 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 953 | }, 954 | "durations": { 955 | "version": "3.4.2", 956 | "resolved": "https://registry.npmjs.org/durations/-/durations-3.4.2.tgz", 957 | "integrity": "sha512-V/lf7y33dGaypZZetVI1eu7BmvkbC4dItq12OElLRpKuaU5JxQstV2zHwLv8P7cNbQ+KL1WD80zMCTx5dNC4dg==" 958 | }, 959 | "entities": { 960 | "version": "2.2.0", 961 | "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", 962 | "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", 963 | "dev": true 964 | }, 965 | "es6-promise": { 966 | "version": "4.2.8", 967 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", 968 | "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" 969 | }, 970 | "fast-xml-parser": { 971 | "version": "3.19.0", 972 | "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz", 973 | "integrity": "sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==", 974 | "dev": true 975 | }, 976 | "form-data": { 977 | "version": "3.0.1", 978 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", 979 | "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", 980 | "requires": { 981 | "asynckit": "^0.4.0", 982 | "combined-stream": "^1.0.8", 983 | "mime-types": "^2.1.12" 984 | } 985 | }, 986 | "loglevel": { 987 | "version": "1.8.0", 988 | "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.0.tgz", 989 | "integrity": "sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==" 990 | }, 991 | "mime-db": { 992 | "version": "1.52.0", 993 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 994 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 995 | }, 996 | "mime-types": { 997 | "version": "2.1.35", 998 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 999 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 1000 | "requires": { 1001 | "mime-db": "1.52.0" 1002 | } 1003 | }, 1004 | "mnemonist": { 1005 | "version": "0.38.3", 1006 | "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", 1007 | "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", 1008 | "dev": true, 1009 | "requires": { 1010 | "obliterator": "^1.6.1" 1011 | } 1012 | }, 1013 | "node-fetch": { 1014 | "version": "2.6.7", 1015 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", 1016 | "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", 1017 | "requires": { 1018 | "whatwg-url": "^5.0.0" 1019 | } 1020 | }, 1021 | "obliterator": { 1022 | "version": "1.6.1", 1023 | "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", 1024 | "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", 1025 | "dev": true 1026 | }, 1027 | "pako": { 1028 | "version": "2.0.4", 1029 | "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", 1030 | "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" 1031 | }, 1032 | "querystringify": { 1033 | "version": "2.2.0", 1034 | "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", 1035 | "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" 1036 | }, 1037 | "requires-port": { 1038 | "version": "1.0.0", 1039 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", 1040 | "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" 1041 | }, 1042 | "tr46": { 1043 | "version": "0.0.3", 1044 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 1045 | "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" 1046 | }, 1047 | "tslib": { 1048 | "version": "2.4.0", 1049 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", 1050 | "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", 1051 | "dev": true 1052 | }, 1053 | "typescript": { 1054 | "version": "4.6.3", 1055 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", 1056 | "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", 1057 | "dev": true 1058 | }, 1059 | "url-parse": { 1060 | "version": "1.5.10", 1061 | "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", 1062 | "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", 1063 | "requires": { 1064 | "querystringify": "^2.1.1", 1065 | "requires-port": "^1.0.0" 1066 | } 1067 | }, 1068 | "uuid": { 1069 | "version": "8.3.2", 1070 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 1071 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", 1072 | "dev": true 1073 | }, 1074 | "webidl-conversions": { 1075 | "version": "3.0.1", 1076 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 1077 | "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" 1078 | }, 1079 | "whatwg-url": { 1080 | "version": "5.0.0", 1081 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 1082 | "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", 1083 | "requires": { 1084 | "tr46": "~0.0.3", 1085 | "webidl-conversions": "^3.0.0" 1086 | } 1087 | } 1088 | } 1089 | } 1090 | -------------------------------------------------------------------------------- /sources/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boilerplate-lambda-typescript", 3 | "version": "1.0.0", 4 | "description": "AWS Lambda Typescript Boilerplate", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "build": "tsc || true", 12 | "prepare": "npm run build" 13 | }, 14 | "author": "Andi Ashari", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/aashari/boilerplate-lambda-typescript/issues" 18 | }, 19 | "homepage": "https://github.com/aashari/boilerplate-lambda-typescript#readme", 20 | "dependencies": { 21 | "@datadog/datadog-api-client": "^1.0.0-beta.9" 22 | }, 23 | "devDependencies": { 24 | "@aws-sdk/client-dynamodb": "^3.76.0", 25 | "@aws-sdk/client-ssm": "^3.75.0", 26 | "@aws-sdk/lib-dynamodb": "^3.76.0", 27 | "@types/aws-lambda": "^8.10.95", 28 | "@types/node": "^17.0.25", 29 | "typescript": "^4.6.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sources/src/decorators/statistic.decorator.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from "util"; 2 | import { DatadogLibrary } from "../libraries/datadog.library"; 3 | 4 | /** 5 | * Statistic decorator to record the execution time of a function/method, additionally it can deliver the metrics to Datadog. 6 | * 7 | * If Datadog delivery is enabled, then there will be 2 metrics + 1 event sent to Datadog: 8 | * 1. The metrics with metric name: .statistic.method_execution_duration which is the execution time of the function/method 9 | * 2. The metrics with metric name: .statistic.method_execution_count which is the number of times the function/method has been executed 10 | * 3. The event with event title: "Method execution failure: ." 11 | * 12 | * All of the metrics and events will be sent to Datadog with the additional default tags: 13 | * 1. class_name: 14 | * 2. method_name: 15 | * 3. status:success|failure 16 | * 17 | * If Datadog delivery is disabled, then the statistic decorator will only record the execution time of the function/method and log the execution time to the console. 18 | * @param isLogToDatadog enable streaming the statistic to datadog 19 | * @returns void 20 | */ 21 | export function statistic(isLogToDatadog: boolean = false) { 22 | // return the decorator function which receives the target function 23 | return function statistic(target: any, propertyKey: string, descriptor: PropertyDescriptor) { 24 | // call the _statistic function with the target function, propertyKey, descriptor and isLogToDatadog 25 | return _statistic(target, propertyKey, descriptor, isLogToDatadog); 26 | } 27 | } 28 | 29 | function _statistic(target: any, propertyKey: string, descriptor: PropertyDescriptor, isLogToDatadog: boolean = false) { 30 | // get the original function and store it in a variable 31 | const originalMethod = descriptor.value; 32 | // override the original function with a new function 33 | descriptor.value = function (...args: any[]) { 34 | // get the start time of the function execution 35 | const start = new Date().getTime(); 36 | // get the class name of the target function 37 | // if the target.constructor.name is equal to "Function", then it means the target is a function/static method 38 | const className = (target.constructor.name != 'Function' ? target.constructor.name : target.name); 39 | // generate default Datadog tags 40 | const datadogTags = [`class_name:${className}`, `method_name:${propertyKey}`] 41 | // run the original function and calculate the execution time 42 | return originalMethod.apply(this, args).then((response: any) => { 43 | // record the execution time 44 | const executionDuration = new Date().getTime() - start; 45 | // if Datadog delivery is not enabled, then log the execution time to the console 46 | if (!isLogToDatadog) { 47 | console.info(`statistic.method-execution-duration:${className}.${propertyKey} ${executionDuration}ms`); 48 | // return the original function response 49 | return response; 50 | } 51 | // if Datadog delivery is enabled, then send the execution time and count to Datadog 52 | DatadogLibrary.queueMetric(`statistic.method-execution-duration`, executionDuration, `gauge`, [...datadogTags, `status:success`]); 53 | DatadogLibrary.queueMetric(`statistic.method-execution-count`, 1, `count`, [...datadogTags, `status:success`]); 54 | // return the original function response 55 | return response; 56 | }).catch((error: any) => { 57 | 58 | // record the execution time 59 | const executionDuration = new Date().getTime() - start; 60 | 61 | // if Datadog delivery is not enabled, then log the execution time to the console 62 | if (!isLogToDatadog) { 63 | console.error(`statistic.method-execution-duration:${className}.${propertyKey} ${executionDuration}ms`); 64 | // throw the original function error 65 | throw error; 66 | } 67 | 68 | // if Datadog delivery is enabled, then send the execution time and count to Datadog 69 | DatadogLibrary.queueMetric(`statistic.method-execution-duration`, executionDuration, `gauge`, [...datadogTags, `status:failure`]); 70 | DatadogLibrary.queueMetric(`statistic.method-execution-count`, 1, `count`, [...datadogTags, `status:failure`]); 71 | 72 | // log the errror message to cloudwatch logs 73 | console.error(`Method ${className}.${propertyKey} execution error`, error); 74 | 75 | // stream the error to datadog event 76 | DatadogLibrary.queueEvent(`Method ${className}.${propertyKey} execution error`, [ 77 | `Class name: ${className}`, 78 | `Method name: ${propertyKey}`, 79 | `Error: ${error}`, 80 | `Error Details: ${inspect(error)}` 81 | ].join(`\n`), "error", [...datadogTags, `status:failure`]); 82 | 83 | // throw the original function error 84 | throw error; 85 | 86 | }); 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /sources/src/functions/booking-create.function.ts: -------------------------------------------------------------------------------- 1 | import { LambdaFunction } from ".."; 2 | import { statistic } from "../decorators/statistic.decorator"; 3 | import { BookingModel } from "../models/booking.model"; 4 | 5 | export class BookingCreateFunction extends LambdaFunction { 6 | 7 | @statistic() 8 | public async handler(event: any, context: any, callback: any) { 9 | console.log('BookingCreateFunction.handler()'); 10 | 11 | let myBookingList: BookingModel[] = [] 12 | 13 | for (let i = 0; i < 100; i++) { 14 | let myBooking = new BookingModel(); 15 | myBooking.id = i.toString(); 16 | myBooking.save(); 17 | myBookingList.push(myBooking); 18 | } 19 | 20 | callback(null, { 21 | statusCode: 200, 22 | body: JSON.stringify(myBookingList), 23 | }); 24 | 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /sources/src/functions/booking-search.function.ts: -------------------------------------------------------------------------------- 1 | import { LambdaFunction } from ".."; 2 | import { statistic } from "../decorators/statistic.decorator"; 3 | import { BookingModel } from "../models/booking.model"; 4 | 5 | export class BookingSearchFunction extends LambdaFunction { 6 | 7 | @statistic() 8 | public async handler(event: any, context: any, callback: any) { 9 | 10 | console.log('BookingSearchFunction.handler()'); 11 | let bookingList = await BookingModel.scan(); 12 | 13 | callback(null, { 14 | statusCode: 200, 15 | body: JSON.stringify(bookingList), 16 | }); 17 | 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /sources/src/functions/flight-search.function.ts: -------------------------------------------------------------------------------- 1 | import { LambdaFunction } from ".."; 2 | import { statistic } from "../decorators/statistic.decorator"; 3 | 4 | export class FlightSearchFunction extends LambdaFunction { 5 | 6 | @statistic() 7 | public async handler(event: any, context: any, callback: any) { 8 | console.log('FlightSearchFunction.handler()'); 9 | callback(null, { 10 | statusCode: 200, 11 | body: JSON.stringify({ 12 | message: 'Hello World!', 13 | }), 14 | }); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /sources/src/helpers/chunk.helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Chunk the given array into chunks of the given size. 3 | * @param array The array to chunk 4 | * @param size The size of the chunk 5 | * @returns The array of chunks 6 | */ 7 | export function chunk(array: any[], size: number): any[][] { 8 | const chunked_arr: any[] = []; 9 | let index = 0; 10 | while (index < array.length) { 11 | chunked_arr.push(array.slice(index, size + index)); 12 | index += size; 13 | } 14 | return chunked_arr; 15 | } 16 | -------------------------------------------------------------------------------- /sources/src/helpers/parameter-store.helper.ts: -------------------------------------------------------------------------------- 1 | import { GetParametersByPathCommand, Parameter, SSMClient } from "@aws-sdk/client-ssm"; 2 | 3 | const SSM_PREFIX = `/service/${process.env.SERVICE_DOMAIN}/${process.env.SERVICE_NAME}/${process.env.SERVICE_ENVIRONMENT}/`; 4 | const SSM_CLIENT = new SSMClient({ 5 | region: process.env.AWS_REGION || 'ap-southeast-1' 6 | }); 7 | 8 | /** 9 | * Parse the parameter store by path and define the environment variables 10 | * @returns Promise<{ [key: string]: string }> The environment variables map 11 | */ 12 | export async function populateEnvironmentVariables(ssmPrefix: string | undefined = undefined, environmentVariablePrefix: string | undefined = undefined) { 13 | 14 | let parameterList: Parameter[] = []; 15 | if (environmentVariablePrefix) environmentVariablePrefix = environmentVariablePrefix.toUpperCase().replace(/[^a-zA-Z0-9]/g, '_'); 16 | 17 | // get all parameter based on the prefix 18 | let nexToken: string | undefined; 19 | while (true) { 20 | let parameterListResponse = await SSM_CLIENT.send(new GetParametersByPathCommand({ 21 | Path: ssmPrefix ? ssmPrefix : SSM_PREFIX, 22 | WithDecryption: true, 23 | NextToken: nexToken 24 | })).catch(e => { 25 | console.error(`[ParameterStoreHelper][populateEnvironmentVariables] failed to get parameters from parameter store with path ${SSM_PREFIX}`, e); 26 | return null; 27 | }); 28 | if (parameterListResponse?.Parameters) { 29 | parameterList = parameterList.concat(parameterListResponse.Parameters); 30 | } 31 | if (!parameterListResponse?.NextToken) break; 32 | nexToken = parameterListResponse.NextToken; 33 | } 34 | 35 | // parse the parameters and set the environment variables 36 | for (let parameter of parameterList) { 37 | if (!parameter?.Name) continue; 38 | // parse the name of the parameter 39 | // this will convert the parameter name to upper case and replace the '-' with '_' 40 | let key = (environmentVariablePrefix ? environmentVariablePrefix + '_' : '') + parameter.Name.replace(ssmPrefix ? ssmPrefix : SSM_PREFIX, '').toUpperCase().replace(/[^a-zA-Z0-9]/g, '_'); 41 | if (key.startsWith(`_`)) key = key.substring(1); 42 | // get the value of the parameter 43 | let value = parameter.Value; 44 | if (!value) continue; 45 | // set the environment variable 46 | console.info(`[ParameterStoreHelper][populateEnvironmentVariables] setting environment variable ${key}`); 47 | process.env[key] = value; 48 | } 49 | 50 | // return all the env 51 | return process.env 52 | 53 | } -------------------------------------------------------------------------------- /sources/src/index.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from "util"; 2 | import { populateEnvironmentVariables } from "./helpers/parameter-store.helper"; 3 | import { DynamoDBLibrary } from "./libraries/dynamodb.library"; 4 | 5 | export interface LambdaFunctionInterface { 6 | handler: (event: any, context: any, callback: any) => Promise; 7 | } 8 | 9 | export class LambdaFunction implements LambdaFunctionInterface { 10 | public handler(event: any, context: any, callback: any) { 11 | console.log('LambdaFunction.handler()'); 12 | return Promise.resolve(event); 13 | } 14 | } 15 | 16 | async function startLambda(event: any, context: any, callback: any) { 17 | 18 | // store the timestamp to calculate the duration 19 | const start = new Date().getTime(); 20 | 21 | // parse the naming based on function name component 22 | let functionServiceName = process.env.SERVICE_NAME ?? `-`; 23 | let functionName = process.env.FUNCTION_NAME ?? `-`; 24 | 25 | // -------------------------------------------------- 26 | // load the helpers here 27 | // -------------------------------------------------- 28 | await populateEnvironmentVariables(); 29 | 30 | // -------------------------------------------------- 31 | // load the library instances here 32 | // -------------------------------------------------- 33 | await DynamoDBLibrary.instance(); 34 | // await DatadogLibrary.instance(); <-- un comment this line to enable datadog library 35 | 36 | // generate class name based on function name 37 | let className = functionName.split(`-`).map((x: string) => x.charAt(0).toUpperCase() + x.slice(1)).join(``) + 'Function'; 38 | 39 | // import the class based on function name and class name, and create an instance of the class 40 | let LambdaFunctionClass = require(`./functions/${functionName}.function`)[className]; 41 | 42 | // create the object of the class 43 | let lambdaFunction = new LambdaFunctionClass(); 44 | 45 | // errror response object 46 | let errorResponse: any | undefined; 47 | 48 | // call the handler function of the object 49 | return lambdaFunction.handler(event, context, callback).catch((error: any) => { 50 | // catching unhandled promise rejections 51 | errorResponse = error; 52 | return; 53 | }).finally(() => { 54 | 55 | console.info(`---------------------------------------------`); 56 | console.info(`lambda function name: ${functionName}`); 57 | console.info(`lambda function service name: ${functionServiceName}`); 58 | console.info(`lambda function duration: ${new Date().getTime() - start} ms`); 59 | console.info(`lambda function left time: ${context.getRemainingTimeInMillis()} ms`); 60 | console.info(`---------------------------------------------`); 61 | 62 | // un comment this line to send the metric to datadog 63 | // let datadogTags = [ 64 | // `name:${functionName}`, 65 | // `status:${errorResponse ? 'failure' : 'success'}` 66 | // ]; 67 | 68 | // // stream the metric to datadog 69 | // DatadogLibrary.queueMetric(`lambda.execution-count`, 1, "count", datadogTags); 70 | // DatadogLibrary.queueMetric(`lambda.execution-duration`, new Date().getTime() - start, "gauge", datadogTags); 71 | // DatadogLibrary.queueMetric(`lambda.execution-left-time`, context.getRemainingTimeInMillis(), "gauge", datadogTags); 72 | 73 | if (!errorResponse) return; 74 | 75 | // log the errror message to cloudwatch logs 76 | console.error(`Lambda ${functionName} execution error`, errorResponse); 77 | 78 | // un comment this line to send the metric to datadog 79 | // // stream the error to datadog event 80 | // DatadogLibrary.queueEvent(`Lambda ${functionName} execution error`, [ 81 | // `Function Name: ${functionName}`, 82 | // `Error: ${errorResponse}`, 83 | // `Error Details: ${inspect(errorResponse)}` 84 | // ].join(`\n`), `error`, datadogTags); 85 | 86 | // throw the original error 87 | callback(null, { 88 | statusCode: 500, 89 | body: JSON.stringify({ 90 | error: errorResponse.message 91 | }) 92 | }); 93 | 94 | }); 95 | } 96 | 97 | export function handler(event: any, context: any, callback: any) { 98 | startLambda(event, context, callback).catch((error: any) => { 99 | console.error(`Lambda execution error`, error); 100 | return Promise.reject(error); 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /sources/src/libraries/datadog.library.ts: -------------------------------------------------------------------------------- 1 | import { v1 as DatadogAPIClient } from '@datadog/datadog-api-client'; 2 | 3 | export class DatadogLibrary { 4 | 5 | private datadogConfiguration: DatadogAPIClient.Configuration; 6 | private datadogMetricsAPI: DatadogAPIClient.MetricsApi; 7 | private datadogEventsAPI: DatadogAPIClient.EventsApi; 8 | 9 | private static datadogLibraryInstance: DatadogLibrary; 10 | 11 | private metricSeries: DatadogAPIClient.Series[] = []; 12 | private metricWaitingHandle: NodeJS.Timeout | undefined; 13 | 14 | private eventSeries: DatadogAPIClient.Event[] = []; 15 | private eventWaitingHandle: NodeJS.Timeout | undefined; 16 | 17 | private defaultDatadogTags = [ 18 | `service:${process.env.SERVICE_NAME}`, 19 | `version:${process.env.SERVICE_VERSION}`, 20 | `function_name:${process.env.FUNCTION_NAME}`, 21 | ]; 22 | 23 | constructor() { } 24 | 25 | public async initiateDatadogConfiguration() { 26 | this.datadogConfiguration = DatadogAPIClient.createConfiguration(); 27 | this.datadogMetricsAPI = new DatadogAPIClient.MetricsApi(this.datadogConfiguration); 28 | this.datadogEventsAPI = new DatadogAPIClient.EventsApi(this.datadogConfiguration); 29 | } 30 | 31 | public static async instance(): Promise { 32 | if (!DatadogLibrary.datadogLibraryInstance) DatadogLibrary.datadogLibraryInstance = new DatadogLibrary(); 33 | await DatadogLibrary.datadogLibraryInstance.initiateDatadogConfiguration(); 34 | return DatadogLibrary.datadogLibraryInstance; 35 | } 36 | 37 | /** 38 | * Queue a metric to be published to Datadog 39 | * @param metricName The name of the metric 40 | * @param metricValue The value of the metric 41 | * @param type The type of the metric (gauge or count) 42 | * @param additionalTags Additional tags to be added to the metric 43 | * @returns boolean indicating whether the metric was queued successfully 44 | */ 45 | public static queueMetric(metricName: string, metricValue: number = 1, type: string = "count", additionalTags: string[] = []) { 46 | 47 | // check whether DD_API_KEY and DD_APP_KEY are set 48 | // if not set please make sure in the main.tf add dd-api-key and dd-app-key in the parameter_store_list 49 | // and make sure change the value in the web console or api console to the value in parameter_store 50 | if (!process.env.DD_APP_KEY || !process.env.DD_APP_KEY || process.env.DD_APP_KEY == 'placeholder' || process.env.DD_APP_KEY == 'placeholder') { 51 | return false; 52 | } 53 | 54 | // generate current metric tag 55 | let currentMetricTag = [...new Set([...this.datadogLibraryInstance.defaultDatadogTags, ...additionalTags])].sort(); 56 | 57 | // metricName replace non alphanumeric and non dot characters 58 | metricName = metricName.replace(/[^a-zA-Z0-9.]/g, '_').toLowerCase(); 59 | 60 | // add current metric to series 61 | this.datadogLibraryInstance.metricSeries.push({ 62 | metric: `${process.env.SERVICE_NAME}.${metricName}`.toLowerCase(), 63 | points: [[Math.round((new Date().getTime() / 1000)), metricValue]], 64 | host: process.env?.AWS_LAMBDA_FUNCTION_NAME ?? "", 65 | type: type, 66 | tags: currentMetricTag.map((tag: string) => tag.toLowerCase()) 67 | }); 68 | 69 | // if there's existing waiting to publish, cancel it 70 | if (this.datadogLibraryInstance.metricWaitingHandle) { 71 | clearTimeout(this.datadogLibraryInstance.metricWaitingHandle); 72 | } 73 | 74 | // if there's no metrics queued for 300ms second, publish them 75 | this.datadogLibraryInstance.metricWaitingHandle = setTimeout(() => { 76 | let currentSeries = JSON.parse(JSON.stringify(this.datadogLibraryInstance.metricSeries)) as DatadogAPIClient.Series[]; 77 | this.datadogLibraryInstance.metricSeries = []; 78 | this.publishMetrics(currentSeries); 79 | }, 300); 80 | 81 | return true; 82 | 83 | } 84 | 85 | /** 86 | * Publish queued metrics to Datadog 87 | * @param currentSeries The series to be published 88 | * @returns Promise indicating whether the metrics were published successfully 89 | */ 90 | private static async publishMetrics(currentSeries: DatadogAPIClient.Series[]) { 91 | if (currentSeries.length == 0) return false; 92 | return this.datadogLibraryInstance.datadogMetricsAPI.submitMetrics({ 93 | body: { series: currentSeries } 94 | }).then((result: any) => { 95 | console.info(`[DatadogLibrary][publishMetrics] successfully published ${currentSeries.length} metrics`, result); 96 | return true; 97 | }).catch((error: any) => { 98 | console.error(`[DatadogLibrary][publishMetrics] failed to publish ${currentSeries.length} metrics with error: ${error}`, error); 99 | return false; 100 | }); 101 | } 102 | 103 | /** 104 | * Queue an event to be published to Datadog 105 | * @param eventName The name of the event 106 | * @param eventText The content/text of the event 107 | * @param additionalTags Additional tags to be added to the event 108 | * @returns boolean indicating whether the event was queued successfully 109 | */ 110 | public static queueEvent(eventName: string, eventText: string, eventType: DatadogAPIClient.EventAlertType = "info", additionalTags: string[] = []) { 111 | // check whether DD_API_KEY and DD_APP_KEY are set 112 | // if not set please make sure in the main.tf add dd-api-key and dd-app-key in the parameter_store_list 113 | // and make sure change the value in the web console or api console to the value in parameter_store 114 | if (!process.env.DD_APP_KEY || !process.env.DD_APP_KEY || process.env.DD_APP_KEY == 'placeholder' || process.env.DD_APP_KEY == 'placeholder') { 115 | return false; 116 | } 117 | 118 | // generate current event tag 119 | let currentEventTag = [...new Set([...this.datadogLibraryInstance.defaultDatadogTags, ...additionalTags])].sort(); 120 | 121 | // add current event to series 122 | this.datadogLibraryInstance.eventSeries.push({ 123 | title: eventName, 124 | text: eventText, 125 | tags: currentEventTag.map((tag: string) => tag.toLowerCase()), 126 | alertType: eventType, 127 | }); 128 | 129 | // if there's existing waiting to publish, cancel it 130 | if (this.datadogLibraryInstance.eventWaitingHandle) { 131 | clearTimeout(this.datadogLibraryInstance.eventWaitingHandle); 132 | } 133 | 134 | // if there's no events queued for 300ms, publish them 135 | this.datadogLibraryInstance.eventWaitingHandle = setTimeout(() => { 136 | let currentSeries = JSON.parse(JSON.stringify(this.datadogLibraryInstance.eventSeries)) as DatadogAPIClient.Event[]; 137 | this.datadogLibraryInstance.eventSeries = []; 138 | this.publishEvents(currentSeries); 139 | }, 300); 140 | 141 | return true; 142 | 143 | } 144 | 145 | /** 146 | * Publish queued events to Datadog 147 | * @param currentSeries The series to be published 148 | * @returns Promise indicating whether the events were published successfully 149 | */ 150 | private static async publishEvents(currentSeries: DatadogAPIClient.Event[]) { 151 | if (currentSeries.length == 0) return false; 152 | let createEventPromises = currentSeries.map((event: DatadogAPIClient.Event) => { 153 | return this.datadogLibraryInstance.datadogEventsAPI.createEvent({ 154 | body: { 155 | title: event.title ?? "", 156 | text: event.text ?? "", 157 | tags: event.tags ?? [], 158 | alertType: event.alertType ?? "info", 159 | } 160 | }); 161 | }); 162 | return Promise.all(createEventPromises).then((result: any) => { 163 | console.info(`[DatadogLibrary][publishEvents] successfully published ${currentSeries.length} events`, result); 164 | return true; 165 | }).catch((error: any) => { 166 | console.error(`[DatadogLibrary][publishEvents] failed to publish ${currentSeries.length} events with error: ${error}`, error); 167 | return false; 168 | }); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /sources/src/libraries/dynamodb.library.ts: -------------------------------------------------------------------------------- 1 | import { AttributeValue, DescribeTableCommand, DynamoDBClient, ScanCommandInput, WriteRequest } from '@aws-sdk/client-dynamodb'; 2 | import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb'; 3 | import { inspect } from 'util'; 4 | import { statistic } from '../decorators/statistic.decorator'; 5 | import { chunk } from '../helpers/chunk.helper'; 6 | import { Model } from '../models/model'; 7 | import { DatadogLibrary } from './datadog.library'; 8 | 9 | interface BATCH_COMMAND { 10 | [key: string]: (Omit & { 11 | PutRequest?: any | undefined; 12 | DeleteRequest?: any | undefined; 13 | })[]; 14 | } 15 | 16 | /** 17 | * DynamoDB library to interact with DynamoDB service, it can be used to create, update, delete, scan and query DynamoDB tables. 18 | * By default, the put and delete methods will be queued and executed in batches, this is to avoid the DynamoDB service from being overloaded. 19 | */ 20 | export class DynamoDBLibrary { 21 | 22 | private static dynamoDBLibrary: DynamoDBLibrary; 23 | 24 | private dynamoDBClient: DynamoDBClient; 25 | private documentClient: DynamoDBDocument; 26 | private dynamodbTableKey: { [tableName: string]: string[] } = {}; 27 | 28 | private putCommandList: BATCH_COMMAND = {}; 29 | private putCommandWaitingHandle: NodeJS.Timeout | undefined; 30 | 31 | private deleteCommandList: BATCH_COMMAND = {}; 32 | private deleteCommandWaitingHandle: NodeJS.Timeout | undefined; 33 | 34 | constructor() { } 35 | 36 | public async initiateDatadogConfiguration() { 37 | this.dynamoDBClient = new DynamoDBClient({ region: process.env.AWS_REGION ?? 'ap-southeast-1' }); 38 | this.documentClient = DynamoDBDocument.from(this.dynamoDBClient); 39 | } 40 | 41 | public static async instance(): Promise { 42 | if (!DynamoDBLibrary.dynamoDBLibrary) DynamoDBLibrary.dynamoDBLibrary = new DynamoDBLibrary(); 43 | await DynamoDBLibrary.dynamoDBLibrary.initiateDatadogConfiguration(); 44 | console.info('[DynamoDBLibrary][instance] DynamoDBLibrary initialized and ready to use'); 45 | return DynamoDBLibrary.dynamoDBLibrary; 46 | } 47 | 48 | private statisticHandler(tableName: string, method: string, data: any | undefined, errorResponse: any | undefined) { 49 | 50 | DatadogLibrary.queueMetric(`dynamodb.${method}`, 1, `count`, [ 51 | `table:${tableName}`, 52 | `class:DynamoDBLibrary`, 53 | `method:${method}`, 54 | `status:${errorResponse ? `failure` : `success`}` 55 | ]); 56 | 57 | if (!errorResponse) return; 58 | 59 | // log the errror message to cloudwatch logs 60 | console.error(`[DynamoDBLibrary][${method}] DynamoDB ${method}.${tableName} execution error`, { errorResponse, data }); 61 | 62 | // stream the error to datadog event 63 | DatadogLibrary.queueEvent(`DynamoDB ${method}.${tableName} execution error`, [ 64 | `Table Name: ${tableName}`, 65 | `Table Data: ${JSON.stringify(data)}`, 66 | `Error: ${errorResponse}`, 67 | `Error Details: ${inspect(errorResponse)}`, 68 | ].join('\n'), "error", [`table:${tableName}`, `class:DynamoDBLibrary`, `method:${method}`]); 69 | 70 | } 71 | 72 | /** 73 | * Get the table key configuration for the table which should contains the primary key and the sort key 74 | * @param tableName the DynamoDB table name 75 | * @returns Promise the list of string contains the pre defined DynamoDB table key attribute name 76 | */ 77 | @statistic() 78 | private static async getTableKey(tableName: string) { 79 | // check if the table key is already cached 80 | if (this.dynamoDBLibrary.dynamodbTableKey[tableName]) return this.dynamoDBLibrary.dynamodbTableKey[tableName]; 81 | // generate empty string list 82 | let tableKey: string[] = []; 83 | // get the table description 84 | let tableDescription = await this.dynamoDBLibrary.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName })); 85 | // get all of the pre defined key attribute name 86 | tableDescription.Table?.KeySchema?.forEach((key) => { 87 | if (key.AttributeName) tableKey.push(key.AttributeName); 88 | }); 89 | // cache the table key attribute name list 90 | this.dynamoDBLibrary.dynamodbTableKey[tableName] = tableKey; 91 | // return the table key attribute name list 92 | return tableKey; 93 | } 94 | 95 | /** 96 | * Get the item from the DynamoDB table by the primary key and the sort key (if available) 97 | * @param tableName the DynamoDB table name 98 | * @param key the primary key and the sort key (if available) 99 | * @returns Promise the item from the DynamoDB table 100 | */ 101 | @statistic() 102 | public static async get(tableName: string, key: { [key: string]: string }): Promise { 103 | // check whether the item is still on the processing put queue 104 | // if exist, return the item from the queue 105 | if (this.dynamoDBLibrary.putCommandList[tableName]) { 106 | let currentPutList: BATCH_COMMAND[string] = this.dynamoDBLibrary.putCommandList[tableName]; 107 | for (let keyName in key) currentPutList = currentPutList.filter((item) => item.PutRequest.Item[keyName] === key[keyName]); 108 | if (currentPutList.length > 0) return currentPutList[0].PutRequest.Item; 109 | } 110 | 111 | // error response record 112 | let errorResponse: any | undefined = undefined; 113 | 114 | // if not exist, get the item from the table, and return the item 115 | return this.dynamoDBLibrary.documentClient.get({ 116 | TableName: tableName, Key: key, 117 | }).then((result) => { 118 | return result.Item ? result.Item as any : null; 119 | }).catch(err => { 120 | errorResponse = err; 121 | return null; 122 | }).finally(() => { 123 | this.dynamoDBLibrary.statisticHandler(tableName, 'get', key, errorResponse); 124 | }) 125 | } 126 | 127 | /** 128 | * Put the item into the DynamoDB table by the primary key and the sort key (if available) 129 | * @param tableName the DynamoDB table name 130 | * @param data the item to be put into the DynamoDB table 131 | * @param isQueue whether the put operation should be queued 132 | * @returns Promise whether the put operation is successful 133 | */ 134 | @statistic() 135 | public static async put(tableName: string, data: Model, isQueue: boolean = true): Promise { 136 | 137 | if (!isQueue) { 138 | // error response record 139 | let errorResponse: any | undefined = undefined; 140 | // put the item into the table, and return the result 141 | return this.dynamoDBLibrary.documentClient.put({ 142 | TableName: tableName, Item: data, 143 | }).then((result) => { 144 | return result.$metadata.httpStatusCode === 200 ? true : false; 145 | }).catch(err => { 146 | errorResponse = err; 147 | return false; 148 | }).finally(() => { 149 | this.dynamoDBLibrary.statisticHandler(tableName, 'put', data, errorResponse); 150 | }); 151 | } 152 | 153 | // generate queue for put command 154 | if (!this.dynamoDBLibrary.putCommandList[tableName]) this.dynamoDBLibrary.putCommandList[tableName] = []; 155 | this.dynamoDBLibrary.putCommandList[tableName].push({ PutRequest: { Item: data } }); 156 | 157 | // if there's existing waiting to publish, cancel it 158 | if (this.dynamoDBLibrary.putCommandWaitingHandle) { 159 | clearTimeout(this.dynamoDBLibrary.putCommandWaitingHandle); 160 | } 161 | 162 | // if there's no metrics queued for 300ms, publish them 163 | this.dynamoDBLibrary.putCommandWaitingHandle = setTimeout(() => { 164 | if (!tableName) return; 165 | let currentPutList: BATCH_COMMAND[string] = JSON.parse(JSON.stringify(this.dynamoDBLibrary.putCommandList[tableName])); 166 | this.dynamoDBLibrary.putCommandList[tableName] = []; 167 | let chunkedList = chunk(currentPutList, 25) as BATCH_COMMAND[string][]; 168 | for (let currentChunk of chunkedList) { 169 | this.batchPutQueueByTable(tableName, currentChunk); 170 | } 171 | }, 300); 172 | 173 | // successfully queued write command 174 | return true; 175 | 176 | } 177 | 178 | /** 179 | * Delete the item from the DynamoDB table by the primary key and the sort key (if available) 180 | * @param tableName the DynamoDB table name 181 | * @param data the item to be deleted from the DynamoDB table 182 | * @param isQueue whether the delete operation should be queued 183 | * @returns Promise whether the delete operation is successful 184 | */ 185 | @statistic() 186 | public static async delete(tableName: string, data: Model | { [key: string]: string }, isQueue: boolean = true): Promise { 187 | 188 | let key = {}; 189 | let tableKey = await this.getTableKey(tableName); 190 | tableKey.forEach((keyName) => key[keyName] = data[keyName]); 191 | 192 | if (!isQueue) { 193 | // error response record 194 | let errorResponse: any | undefined = undefined; 195 | // delete the item from the table, and return the result 196 | return this.dynamoDBLibrary.documentClient.delete({ 197 | TableName: tableName, Key: key, 198 | }).then((result) => { 199 | return result.$metadata.httpStatusCode === 200 ? true : false; 200 | }).catch(err => { 201 | errorResponse = err; 202 | return false; 203 | }).finally(() => { 204 | this.dynamoDBLibrary.statisticHandler(tableName, 'delete', key, errorResponse); 205 | }); 206 | } 207 | 208 | // generate queue for delete command 209 | if (!this.dynamoDBLibrary.deleteCommandList[tableName]) this.dynamoDBLibrary.deleteCommandList[tableName] = []; 210 | this.dynamoDBLibrary.deleteCommandList[tableName].push({ DeleteRequest: { Key: key } }); 211 | 212 | // if there's existing waiting to publish, cancel it 213 | if (this.dynamoDBLibrary.deleteCommandWaitingHandle) { 214 | clearTimeout(this.dynamoDBLibrary.deleteCommandWaitingHandle); 215 | } 216 | 217 | // if there's no metrics queued for 300ms, publish them 218 | this.dynamoDBLibrary.deleteCommandWaitingHandle = setTimeout(() => { 219 | if (!tableName) return; 220 | let currentDeleteList: BATCH_COMMAND[string] = JSON.parse(JSON.stringify(this.dynamoDBLibrary.deleteCommandList[tableName])); 221 | this.dynamoDBLibrary.deleteCommandList[tableName] = []; 222 | let chunkedList = chunk(currentDeleteList, 25) as BATCH_COMMAND[string][]; 223 | for (let currentChunk of chunkedList) { 224 | this.batchDeleteQueueByTable(tableName, currentChunk); 225 | } 226 | }, 300); 227 | 228 | // successfully queued write command 229 | return true; 230 | 231 | } 232 | 233 | /** 234 | * Scan the items from the DynamoDB table 235 | * @param tableName the DynamoDB table name 236 | * @param filter the filter to be applied to the scan operation 237 | * @param operand the operand to be applied to the filter 238 | * @returns Promise the items from the DynamoDB table 239 | */ 240 | @statistic() 241 | public static async scan(tableName: string, filter: { [key: string]: string } | undefined = undefined, operand: string = 'AND'): Promise { 242 | let resultData: Model[] = []; 243 | let lastEvaluatedKey: { [key: string]: AttributeValue } | undefined; 244 | while (true) { 245 | let params: ScanCommandInput = { 246 | TableName: tableName, 247 | ExclusiveStartKey: lastEvaluatedKey, 248 | } 249 | if (filter) { 250 | params.FilterExpression = Object.keys(filter).map((key) => `contains(#${key}, :${key})`).join(` ${operand} `); 251 | params.ExpressionAttributeNames = Object.keys(filter).reduce((acc, key) => { 252 | acc[`#${key}`] = key; 253 | return acc; 254 | }, {}); 255 | params.ExpressionAttributeValues = Object.keys(filter).reduce((acc, key) => { 256 | acc[`:${key}`] = filter[key]; 257 | return acc; 258 | }, {}) 259 | } 260 | // error response record 261 | let errorResponse: any | undefined = undefined; 262 | // scan the items from the table, and return the result 263 | let isSuccess = await this.dynamoDBLibrary.documentClient.scan(params).then((result) => { 264 | lastEvaluatedKey = result.LastEvaluatedKey; 265 | resultData = resultData.concat(result.Items as Model[]); 266 | return result.$metadata.httpStatusCode === 200 ? true : false; 267 | }).catch(err => { 268 | errorResponse = err; 269 | return false; 270 | }).finally(() => { 271 | this.dynamoDBLibrary.statisticHandler(tableName, 'scan', { filter, params }, errorResponse); 272 | }); 273 | if (!isSuccess) break; 274 | if (!lastEvaluatedKey) break; 275 | } 276 | return resultData; 277 | } 278 | 279 | /** 280 | * Batch put the items into the DynamoDB table 281 | * @param tableName the DynamoDB table name 282 | * @param currentPutList the list of items to be put into the DynamoDB table 283 | * @returns Promise whether the put operation is successful 284 | */ 285 | @statistic() 286 | private static async batchPutQueueByTable(tableName: string, currentPutList: BATCH_COMMAND[string]) { 287 | 288 | let tableKey = await this.getTableKey(tableName); 289 | let uniquePutList: BATCH_COMMAND[string] = []; 290 | 291 | currentPutList = currentPutList.reverse(); 292 | currentPutList.forEach((item) => { 293 | let isUnique: boolean[] = []; 294 | tableKey.forEach((keyName) => { 295 | if (uniquePutList.find((uniqueItem) => uniqueItem.PutRequest.Item[keyName] === item.PutRequest.Item[keyName])) isUnique.push(false); 296 | }); 297 | if (isUnique.length != tableKey.length) uniquePutList.push(item); 298 | }); 299 | 300 | if (uniquePutList.length === 0) return false; 301 | // error response record 302 | let errorResponse: any | undefined = undefined; 303 | // batch put the items into the table, and return the result 304 | return this.dynamoDBLibrary.documentClient.batchWrite({ 305 | RequestItems: { [tableName]: uniquePutList }, 306 | }).then((result) => { 307 | console.info(`[dynamodblibrary][batchPutQueueByTable] successfully put ${uniquePutList.length} items to table ${tableName}`); 308 | return result.$metadata.httpStatusCode === 200 ? true : false; 309 | }).catch(err => { 310 | errorResponse = err; 311 | return false; 312 | }).finally(() => { 313 | this.dynamoDBLibrary.statisticHandler(tableName, 'batchPut', uniquePutList, errorResponse); 314 | }); 315 | } 316 | 317 | /** 318 | * Batch delete the items from the DynamoDB table 319 | * @param tableName the DynamoDB table name 320 | * @param currentDeleteList the list of items to be deleted from the DynamoDB table 321 | * @returns Promise whether the delete operation is successful 322 | */ 323 | @statistic() 324 | private static async batchDeleteQueueByTable(tableName: string, currentDeleteList: BATCH_COMMAND[string]) { 325 | 326 | let tableKey = await this.getTableKey(tableName); 327 | let uniqueDeleteList: BATCH_COMMAND[string] = []; 328 | 329 | currentDeleteList = currentDeleteList.reverse(); 330 | currentDeleteList.forEach((item) => { 331 | let isUnique: boolean[] = []; 332 | tableKey.forEach((keyName) => { 333 | if (uniqueDeleteList.find((uniqueItem) => uniqueItem.DeleteRequest.Key[keyName] === item.DeleteRequest.Key[keyName])) isUnique.push(false); 334 | }); 335 | if (isUnique.length != tableKey.length) uniqueDeleteList.push(item); 336 | }); 337 | 338 | if (uniqueDeleteList.length === 0) return false; 339 | // error response record 340 | let errorResponse: any | undefined = undefined; 341 | // batch delete the items from the table, and return the result 342 | return this.dynamoDBLibrary.documentClient.batchWrite({ 343 | RequestItems: { [tableName]: uniqueDeleteList }, 344 | }).then((result) => { 345 | console.info(`[dynamodblibrary][batchDeleteQueueByTable] successfully delete ${uniqueDeleteList.length} data from table ${tableName}`); 346 | return result.$metadata.httpStatusCode === 200 ? true : false; 347 | }).catch(err => { 348 | errorResponse = err; 349 | return false; 350 | }).then(() => { 351 | this.dynamoDBLibrary.statisticHandler(tableName, 'batchDelete', uniqueDeleteList, errorResponse); 352 | }); 353 | 354 | } 355 | 356 | } 357 | -------------------------------------------------------------------------------- /sources/src/models/booking.model.ts: -------------------------------------------------------------------------------- 1 | import { Model } from './model'; 2 | 3 | export class BookingModel extends Model { 4 | public id: string; 5 | // insert your model properties here 6 | // e.g. public name: string; 7 | } 8 | -------------------------------------------------------------------------------- /sources/src/models/flight.model.ts: -------------------------------------------------------------------------------- 1 | import { Model } from './model'; 2 | 3 | export class FlightModel extends Model { 4 | public id: string; 5 | // insert your model properties here 6 | // e.g. public name: string; 7 | } 8 | -------------------------------------------------------------------------------- /sources/src/models/model.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBLibrary } from "../libraries/dynamodb.library"; 2 | 3 | export class Model { 4 | 5 | public created_at?: number; 6 | public updated_at?: number; 7 | 8 | constructor(data?: Model) { 9 | if (!data) return this; 10 | for (let key in data) this[key] = data[key]; 11 | return this; 12 | } 13 | 14 | /** 15 | * Generate the actual DynamoDB table name for the given Model class name. 16 | * @param className The name of the Model class. 17 | * @returns string of the actual DynamoDB table name. 18 | */ 19 | static getTableName(className: string | undefined = undefined): string { 20 | let environmentName = `DYNAMODB_TABLE_${(className ? className : this.name).replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()}`; 21 | let environmentNameComponent = environmentName.split('_'); environmentNameComponent.pop(); 22 | let tableName = process.env[environmentNameComponent.join('_').toUpperCase()]; 23 | if (!tableName) throw new Error(`[Model][getTableName] table name not found for ${environmentName}`); 24 | return tableName; 25 | } 26 | 27 | /** 28 | * Compare the given object with the current object, and update the current object with the given object. 29 | * @param newData The new data to update the current object with. 30 | * @returns string[] The list of keys that are updated. 31 | */ 32 | public compareAndSave(newData: Model) { 33 | let updatedFields: string[] = [] 34 | let newDataObject = JSON.parse(JSON.stringify(newData)); 35 | for (let key in newDataObject) if (JSON.stringify(newDataObject[key]) !== JSON.stringify(this[key])) { 36 | this[key] = newDataObject[key]; 37 | updatedFields.push(key); 38 | } 39 | return updatedFields; 40 | } 41 | 42 | /** 43 | * Save the current object to the DynamoDB table. 44 | * @returns Promise Whether the save operation is successful. 45 | */ 46 | public async save() { 47 | return Model.put(this, this.constructor.name); 48 | } 49 | 50 | /** 51 | * Delete the current object from the DynamoDB table. 52 | * @returns Promise Whether the delete operation is successful. 53 | */ 54 | public async delete() { 55 | return Model.delete(this, this.constructor.name); 56 | } 57 | 58 | /** 59 | * Static method to get the object by the given key. 60 | * @param key The key to get the object by. 61 | * @returns Promise The object if found, otherwise null. 62 | */ 63 | public static async get(key: { [key: string]: string }): Promise { 64 | let tableName = this.getTableName(); 65 | return DynamoDBLibrary.get(tableName, key); 66 | } 67 | 68 | /** 69 | * Static method to put the object to the DynamoDB table. 70 | * @param data The object to put to the DynamoDB table. 71 | * @param className The name of the Model class. 72 | * @returns Promise Whether the put operation is successful. 73 | */ 74 | public static async put(data: Model, className: string | undefined = undefined): Promise { 75 | let tableName = this.getTableName(className); 76 | if (!data.created_at) data.created_at = Date.now(); 77 | data.updated_at = Date.now(); 78 | return DynamoDBLibrary.put(tableName, data); 79 | } 80 | 81 | /** 82 | * Static method to scan the DynamoDB table by the given filter and return the list of objects. 83 | * @param query The filter to scan the DynamoDB table by. 84 | * @returns Promise The list of objects. 85 | */ 86 | public static async scan(query: { [key: string]: string } | undefined = undefined): Promise { 87 | let tableName = this.getTableName(); 88 | return DynamoDBLibrary.scan(tableName, query); 89 | } 90 | 91 | /** 92 | * Static method to delete the object by the given key. 93 | * @param data The key to delete the object by. 94 | * @param className The name of the Model class. 95 | * @returns Promise Whether the delete operation is successful. 96 | */ 97 | public static async delete(data: Model | { [key: string]: string }, className: string | undefined = undefined): Promise { 98 | let tableName = this.getTableName(className); 99 | return DynamoDBLibrary.delete(tableName, data); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /sources/src/models/transaction.model.ts: -------------------------------------------------------------------------------- 1 | import { Model } from './model'; 2 | 3 | export class FlightModel extends Model { 4 | public booking_id: string; 5 | public flight_id: string; 6 | // insert your model properties here 7 | // e.g. public name: string; 8 | } 9 | -------------------------------------------------------------------------------- /sources/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "strictNullChecks": true, 5 | "experimentalDecorators": true, 6 | "target": "es6", 7 | "outDir": "dist", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "sourceMap": true, 11 | "esModuleInterop": true, 12 | "lib": ["es2016", "dom"], 13 | "rootDir": "src" 14 | }, 15 | "include": ["src"], 16 | "exclude": ["node_modules", "dist"] 17 | } 18 | -------------------------------------------------------------------------------- /terraform.tfvars.example: -------------------------------------------------------------------------------- 1 | # service version (e.g. v1.0.0) 2 | service_version = "v1.0.0" 3 | 4 | # service domain is the level 1 logical stacks grouping 5 | service_domain = "flight" 6 | 7 | # service name is the level 2 logical stacks grouping, this will be used as the prefix for the stack name 8 | service_name = "booking" 9 | 10 | # service environment is the environment of the service 11 | service_environment = "dev" 12 | 13 | # following configuration is an optional configuration 14 | # generate SSM Parameter Store which will be loaded into the Lambda Function environment 15 | parameter_store_list = [ 16 | "dd-api-key", // this will be loaded into the Lambda Function environment as DD_API_KEY 17 | "dd-app-key", // this will be loaded into the Lambda Function environment as DD_APP_KEY 18 | ] 19 | 20 | # following configuration is an optional configuration 21 | # this will create a DynamoDB Table and SSM Parameter Store to store the table name 22 | # the parameterstore will be loaded into the Lambda Function environment as DYNAMO_DB_TABLE_${each.name} 23 | dynamodb_table_list = [ 24 | { 25 | name : "booking", 26 | key : "id", 27 | }, 28 | { 29 | name : "flight", 30 | key : "id", 31 | }, 32 | { 33 | name : "transaction", 34 | key : "booking_id", 35 | range_key : "flight_id", 36 | } 37 | ] 38 | 39 | # following configuration is an optional configuration 40 | # leave this as empty object if you don't want to use Lambda Function custom configuration 41 | lambda_function_configuration = { 42 | booking-create : { 43 | lambda_memory_size : "128", 44 | lambda_timeout : "60", 45 | schedule_expression : "rate(1 minute)", // this will trigger the Lambda Function every minute 46 | } 47 | } 48 | 49 | # define the default tags for the resources 50 | default_tags = {} 51 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "service_version" { 2 | default = "v1.0.0" 3 | type = string 4 | description = "The version of the service" 5 | } 6 | 7 | variable "service_domain" { 8 | type = string 9 | description = "The 1st level of logical grouping of the service, e.g. 'api', 'web', 'db', etc." 10 | } 11 | 12 | variable "service_name" { 13 | type = string 14 | description = "The 2nd level of logical grouping of the service, e.g. 'my-api', 'my-web', 'my-db', etc." 15 | } 16 | 17 | variable "service_environment" { 18 | type = string 19 | description = "The 3rd level of logical grouping of the service, e.g. 'dev', 'test', 'prod', etc." 20 | } 21 | 22 | variable "parameter_store_list" { 23 | type = list(string) 24 | default = [] 25 | description = <[
  "datadog-api-key",
  "datadog-app-key",
  "sentry-dsn",
  "sentry-environment"
] 28 | EOF 29 | } 30 | 31 | variable "dynamodb_table_list" { 32 | type = list(object({ 33 | name = string, 34 | key = string, 35 | range_key = optional(string), 36 | })) 37 | default = [] 38 | description = <[
  {
    "name": "booking",
    "key": "id"
  },
  {
    "name": "flight",
    "key": "id"
  },
  {
    "name": "transaction",
    "key": "booking_id",
    "range_key": "flight_id"
  }
] 41 | EOF 42 | } 43 | 44 | variable "lambda_function_configuration" { 45 | type = map(object({ 46 | lambda_memory_size = optional(number), 47 | lambda_timeout = optional(number), 48 | schedule_expression = optional(string), 49 | })) 50 | default = {} 51 | description = <{
  "booking-create": {
    "lambda_memory_size": 1024,
    "lambda_timeout": 300
  }
} 54 | EOF 55 | } 56 | 57 | variable "default_tags" { 58 | type = map(string) 59 | default = {} 60 | description = "The default tags for the service" 61 | } 62 | --------------------------------------------------------------------------------