diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f5d09fc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+.vscode/*
+node_modules/
+dist/
+temp/
+deploy/
+global-s3-assets/
+regional-s3-assets/
+*.build.log
+tsconfig.tsbuildinfo
+*.zip
+.pnpm-debug.log
+pnpm-lock.yaml
+repo-state.json
+app-config-dev.json
+app-config-prod.json
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..31214d8
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,22 @@
+# Boilerplate CHANGELOG.md
+
+## 1.0.0
+
+### Notes
+
+- [X] Docs: add CHANGELOG.md
+- [X] Docs: What to do after cloned the boilerplate
+- [X] Docs: for updating source/common/config/rush/deploy.json
+- [X] Remove .vscode from git
+- [X] Fix: The location of compiled temp file problem
+- [X] Logger: Declare/export logger in utils, can set level and can be used directly
+- [X] Folder structure: The structure of service source (src)
+- [X] Folder structure: The naming of source, want to plus "handler" to api controller
+- [X] Fix: cdk deploy same s3/bootstrapping problem
+- [X] Fix: cdk deploy can set env parameters
+- [X] Create CDK custom template
+- [X] Test: add integration test using Cucumber.js
+- [X] Feat: add destroy stacks script
+- [X] Test: add code coverage report using Jest
+- [X] Feat: add build stacks script and publish packages to npm registry
+- [X] Util: managing environment variables of business service
diff --git a/PREPARE.md b/PREPARE.md
new file mode 100644
index 0000000..8273ec5
--- /dev/null
+++ b/PREPARE.md
@@ -0,0 +1,68 @@
+# PREPARE
+
+## jq
+
+```sh
+$sudo apt update
+$sudo apt install -y jq
+```
+
+## aws-cli
+
+```sh
+$curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
+$unzip awscliv2.zip
+$sudo ./aws/install
+$/usr/local/bin/aws --version
+aws-cli/2.11.2 Python/3.11.2 Linux/4.14.305-227.531.amzn2.x86_64 exe/x86_64.amzn.2 prompt/off
+```
+
+## nodejs
+
+```sh
+$nvm install 16
+$nvm use 16
+Now using node v16.19.1 (npm v8.19.3)
+
+$node --version
+v16.19.1
+```
+
+## pnpm
+
+```sh
+npm install -g npm@9.6.1
+npm install -g pnpm@6.4.0
+```
+
+## git
+
+```sh
+$git --version
+git version 2.39.2
+```
+
+## rush
+
+```bash
+$git config --global user.name "sample"
+$git config --local user.email "mrexample@users.noreply.github.com"
+$npm install -g @microsoft/rush
+$git clone https://github.com/microsoft/rushstack
+$cd rushstack
+$rush update
+$rush rebuild
+$rush build
+$rush rebuild --verbose
+```
+
+## aws profile(if necessary)
+
+```bash
+aws configue
+cat ~/.aws/credentials
+[defaule]
+aws_access_key_id=xxx
+aws_secret_access_key=yyy
+aws_session_token=zzz
+```
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cd3bf61
--- /dev/null
+++ b/README.md
@@ -0,0 +1,108 @@
+# Monorepo Microservices Boilerplate
+
+This is microservices boilerplate for monorepo with CDK and Rush.
+
+- CDK is used for producing CloudFormation output and microservice deployments
+- Rush.js is used for managing dependencies and compiling multiple business service repos
+
+![CDK Intro](docs/assets/aws_cdk_intro.png)
+
+![Rush Monorepo](docs/assets/monorepo.png)
+
+## 1. Get started
+
+### 1.1. Clone the boilerplate repo from Github
+
+```bash
+ $git clone https://github.com/hanhdt/ts-microservices-rush-boilerplate.git
+```
+
+### 1.2. Rename the boilerplate repo to your own repo name
+
+## 2. Prerequisites
+
+- see PREPARE.md
+
+### AWS Account & IAM User
+
+First of all, AWS Account and IAM User is required. IAM user's credential keys also are required.
+
+### Dependencies
+
+To execute this template codes, the following modules must be installed.
+
+- AWS CLI: aws --version
+
+- Node.js: node --version
+
+- AWS CDK: cdk --version
+
+- Rush: rush --version
+
+## 3. Setup target environment configuration (for example: dev)
+
+![app-config-demo](docs/assets/app-config-sample.png)
+
+Because we need to ensure that a single source code is maintained, all configurations are managed in config/app-config-[your-suffix].json. And several files are made according to the environment you want to deploy to, and you have to choose one of them when deploying.
+For example, if you want to deploy to dev environment, you have to create `config/app-config-dev.json`
+
+- reference to config/app-config-sample.json
+
+## 4. Initial setup and bootstrap
+
+Execute the following command:
+
+```bash
+ $./script/initial_setup.sh config/app-config-dev.json
+```
+
+## 5. Init a new service repo (for example: sample-service)
+
+
+
+To create new service repository, do the following step:
+
+- Change `App` to your new `service-folder` name in `config/app-config-[environment].json`
+
+- Execute the following command:
+
+ ```bash
+ $./script/create_new_service.sh config/app-config-dev.json
+ ```
+
+- Rename `@hanhdt/sample-service` in `package.json` to your new `service-name`
+
+## 6. Deploy procedure
+
+To deploy stacks, run the following commands:
+
+```bash
+ $./script/deploy_stacks.sh config/app-config-dev.json
+```
+
+Read more details about adding Rush configuration at `script/README.md`
+
+## 7. Local Test
+
+- see source/package/services/sample-service/README.md
+
+## Appendixes
+
+### Fix error: "SSM parameter /cdk-bootstrap/hnb659fds/version not found" in the Step 7
+
+```bash
+ $cd source/packages/services/
+ $cdk deploy --no-previous-parameters -c config=dev
+```
+
+### Create local configuration file
+
+The `.env` file will be placed at the service folder.
+And it is scoped to the service.
+
+You can put `.env` file in the `source/packages/services//config` folder.
+
+```bash
+ $cd source/packages/services//config
+ $touch .env
+```
diff --git a/config/app-config-sample.json b/config/app-config-sample.json
new file mode 100644
index 0000000..a1047bb
--- /dev/null
+++ b/config/app-config-sample.json
@@ -0,0 +1,15 @@
+{
+ "AWSAccountID": "",
+ "AWSProfileName": "",
+ "AWSProfileRegion": "ap-northeast-1",
+
+ "Solution": "",
+ "Environment": "dev",
+ "App": "",
+
+ "Version": "0.0.1",
+ "Build": "0",
+ "QualifierCustom": "hnb659fds",
+
+ "LogLevel": "info"
+}
diff --git a/docs/assets/add-rush-configs.png b/docs/assets/add-rush-configs.png
new file mode 100644
index 0000000..429a582
Binary files /dev/null and b/docs/assets/add-rush-configs.png differ
diff --git a/docs/assets/app-config-sample.png b/docs/assets/app-config-sample.png
new file mode 100644
index 0000000..eeccaed
Binary files /dev/null and b/docs/assets/app-config-sample.png differ
diff --git a/docs/assets/aws_cdk_intro.png b/docs/assets/aws_cdk_intro.png
new file mode 100644
index 0000000..47124de
Binary files /dev/null and b/docs/assets/aws_cdk_intro.png differ
diff --git a/docs/assets/monorepo.png b/docs/assets/monorepo.png
new file mode 100644
index 0000000..b7c3698
Binary files /dev/null and b/docs/assets/monorepo.png differ
diff --git a/docs/assets/service-repo-sample.png b/docs/assets/service-repo-sample.png
new file mode 100644
index 0000000..147a539
Binary files /dev/null and b/docs/assets/service-repo-sample.png differ
diff --git a/script/README.md b/script/README.md
new file mode 100644
index 0000000..8ab6050
--- /dev/null
+++ b/script/README.md
@@ -0,0 +1,48 @@
+# Deployment services
+
+## 1. Update Rush configuration files
+
+Add your service project to
+
+* source/rush.json
+* source/common/config/rush/deploy.json
+
+![Add Rush Config](../docs/assets/add-rush-configs.png)
+
+## 1. Quick deploy
+
+This script will deploy all services under `source/packages/services` folder and its infrastructure to AWS.
+
+```sh
+$cd ~/ts-monorepo-microservices-boilerplate
+$chmod +x ./deployment/*.sh
+$./deployment/deploy_stacks.sh config/app-config-dev.json
+```
+
+## 2. Single service deploy
+
+If you want to deploy a single service, you can use the following command.
+
+```sh
+$cd ~/source/packages/services/sample-service
+$npm run deploy:[dev|prod]
+```
+
+## 3. Confirm deployed services
+
+```sh
+$ curl --location --request GET 'https://x6wlyex6pg.execute-api.ap-northeast-1.amazonaws.com/prod/canframes/?sort=asc' --header 'Accept: application/vnd.aws-cdf-v1.0+json' --header 'Content-Type: application/vnd.aws-cdf-v1.0+json' --header 'Content-Type: text/plain'
+{"canframemodels":[{"year":"2023","model":"sample1-01"},{"year":"2023","model":"sample1-02"}]}
+
+
+$ curl --location --request GET 'https://wnsjf6x03g.execute-api.ap-northeast-1.amazonaws.com/prod/canframes/?sort=asc' --header 'Accept: application/vnd.aws-cdf-v1.0+json' --header 'Content-Type: application/vnd.aws-cdf-v1.0+json' --header 'Content-Type: text/plain'
+{"canframemodels":[{"year":"2023","model":"sample2-01"},{"year":"2023","model":"sample2-02"}]}
+```
+
+## 4. Destroy all services
+
+```sh
+$cd ~/ts-monorepo-microservices-boilerplate
+$chmod +x ./deployment/*.sh
+$./script/destroy_stacks.sh config/app-config-dev.json
+```
diff --git a/script/build_stacks.sh b/script/build_stacks.sh
new file mode 100755
index 0000000..eda6554
--- /dev/null
+++ b/script/build_stacks.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+# Configuration File Path
+export GLOBAL_APP_CONFIG=$1
+export PROJECT_STAGE=$(cat $GLOBAL_APP_CONFIG | jq -r '.Environment') #ex> development
+export SOLUTION_NAME=$(cat $GLOBAL_APP_CONFIG | jq -r '.Solution')
+export APP_NAME=$(cat $GLOBAL_APP_CONFIG | jq -r '.App')
+
+currntPWD=`pwd`
+cd ${currntPWD}/source/
+
+echo ==--------CheckDedendencies---------==
+aws --version
+node --version
+npm --version
+cdk --version
+rush version
+pnpm --version
+git --version
+jq --version
+
+echo ==--------ConfigInfo---------==
+echo $GLOBAL_APP_CONFIG
+echo $AWS_PROFILE
+echo $PROJECT_STAGE
+echo $SOLUTION_NAME
+echo $APP_NAME
+echo .
+echo .
+
+echo ==--------BuildEnvironment---------==
+# rush purge
+rush install
+rush update
+rush rebuild
+
+echo .
+echo .
+
+echo "Build Stacks Completed"
+
+exit
diff --git a/script/create_new_service.sh b/script/create_new_service.sh
new file mode 100755
index 0000000..13bb8c0
--- /dev/null
+++ b/script/create_new_service.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# Configuration File Path
+export GLOBAL_APP_CONFIG=$1
+export SERVICE_NAME=$2
+export SOLUTION_NAME=$(cat $GLOBAL_APP_CONFIG | jq -r '.Solution')
+export APP_NAME=$(cat $GLOBAL_APP_CONFIG | jq -r '.App')
+export PROJECT_STAGE=$(cat $GLOBAL_APP_CONFIG | jq -r '.Environment') #ex> development
+
+echo ==--------ConfigInfo---------==
+echo "$GLOBAL_APP_CONFIG"
+echo "Profile: $AWS_PROFILE"
+echo "Env: $PROJECT_STAGE"
+echo "Solution: $SOLUTION_NAME"
+echo "App: $APP_NAME"
+echo "Service: $SERVICE_NAME"
+echo .
+echo .
+
+echo ==--------CreateServiceRepo---------==
+currntPWD=`pwd`
+cd ${currntPWD}/source/packages/services
+mkdir $SERVICE_NAME
+SAMPLE_REPO_DIR="${currntPWD}/source/packages/services/sample-service"
+cd $SAMPLE_REPO_DIR
+tar cf - --exclude="./dist" --exclude="./node_modules" --exclude="./cdk.out" . | (cd ${currntPWD}/source/packages/services/$SERVICE_NAME && tar xvf -)
+echo .
+echo .
+exit
\ No newline at end of file
diff --git a/script/deploy_stacks.sh b/script/deploy_stacks.sh
new file mode 100755
index 0000000..d819593
--- /dev/null
+++ b/script/deploy_stacks.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+
+# Configuration File Path
+export GLOBAL_APP_CONFIG=$1
+export PROJECT_STAGE=$(cat $GLOBAL_APP_CONFIG | jq -r '.Environment') #ex> development
+export SOLUTION_NAME=$(cat $GLOBAL_APP_CONFIG | jq -r '.Solution')
+export APP_NAME=$(cat $GLOBAL_APP_CONFIG | jq -r '.App')
+
+currntPWD=`pwd`
+cd ${currntPWD}/source/
+
+echo ==--------CheckDedendencies---------==
+# npm install -g aws-cdk
+aws --version
+node --version
+npm --version
+cdk --version
+rush version
+pnpm --version
+git --version
+jq --version
+
+echo ==--------ConfigInfo---------==
+echo $GLOBAL_APP_CONFIG
+echo $AWS_PROFILE
+echo $PROJECT_STAGE
+echo $SOLUTION_NAME
+echo $APP_NAME
+echo .
+echo .
+
+echo ==--------BuildEnvironment---------==
+# rush purge
+rush install
+rush update
+rush rebuild
+
+echo .
+echo .
+
+echo ==--------DeployStacks---------==
+rush deploy-services
+
+echo .
+echo .
+echo "Deploy Stacks Completed"
+
+exit
diff --git a/script/destroy_stacks.sh b/script/destroy_stacks.sh
new file mode 100755
index 0000000..fcc6b74
--- /dev/null
+++ b/script/destroy_stacks.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# Configuration File Path
+export GLOBAL_APP_CONFIG=$1
+export PROJECT_STAGE=$(cat $GLOBAL_APP_CONFIG | jq -r '.Environment') #ex> development
+export SOLUTION_NAME=$(cat $GLOBAL_APP_CONFIG | jq -r '.Solution')
+export APP_NAME=$(cat $GLOBAL_APP_CONFIG | jq -r '.App')
+
+currntPWD=`pwd`
+cd ${currntPWD}/source/
+
+echo ==--------CheckDedendencies---------==
+# npm install -g aws-cdk
+aws --version
+node --version
+npm --version
+cdk --version
+rush version
+pnpm --version
+git --version
+jq --version
+
+echo ==--------ConfigInfo---------==
+echo $GLOBAL_APP_CONFIG
+echo $AWS_PROFILE
+echo $PROJECT_STAGE
+echo $SOLUTION_NAME
+echo $APP_NAME
+echo .
+echo .
+
+# rush purge
+
+echo .
+echo .
+
+echo ==--------DestroyStacks---------==
+rush destroy-services
+
+exit
diff --git a/script/initial_setup.sh b/script/initial_setup.sh
new file mode 100755
index 0000000..4f198c5
--- /dev/null
+++ b/script/initial_setup.sh
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+# Configuration File Path
+export GLOBAL_APP_CONFIG=$1
+
+export AWS_ACCOUNT=$(cat $GLOBAL_APP_CONFIG | jq -r '.AWSAccountID') #ex> 123456789123
+export AWS_REGION=$(cat $GLOBAL_APP_CONFIG | jq -r '.AWSProfileRegion') #ex> us-east-1
+export AWS_PROFILE=$(cat $GLOBAL_APP_CONFIG | jq -r '.AWSProfileName') #ex> cdk-demo
+export CDK_QUALIFIER=$(cat $GLOBAL_APP_CONFIG | jq -r '.QualifierCustom') #ex> hnb659fds
+
+currntPWD=`pwd`
+cd ${currntPWD}/source/
+
+echo ==--------CheckDedendencies---------==
+# npm install -g aws-cdk
+aws --version
+node --version
+npm --version
+cdk --version
+rush version
+pnpm --version
+git --version
+jq --version
+
+echo ==--------ConfigInfo---------==
+echo $GLOBAL_APP_CONFIG
+echo $AWS_ACCOUNT
+echo $AWS_REGION
+echo $AWS_PROFILE
+echo $CDK_QUALIFIER
+echo .
+echo .
+
+echo ==--------BootstrapCDKEnvironment---------==
+if [ -z "$AWS_PROFILE" ]; then
+ cdk bootstrap aws://$AWS_ACCOUNT/$AWS_REGION --qualifier $CDK_QUALIFIER
+else
+ cdk bootstrap aws://$AWS_ACCOUNT/$AWS_REGION --profile $AWS_PROFILE --qualifier $CDK_QUALIFIER
+fi
+echo .
+echo .
+
+echo ==--------InstallPackages---------==
+rush install
+rush update
+rush rebuild
+
+echo .
+echo .
diff --git a/script/publish_packages.sh b/script/publish_packages.sh
new file mode 100755
index 0000000..393f8cf
--- /dev/null
+++ b/script/publish_packages.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+
+# Configuration File Path
+export GLOBAL_APP_CONFIG=$1
+export PROJECT_STAGE=$(cat $GLOBAL_APP_CONFIG | jq -r '.Environment') #ex> development
+export SOLUTION_NAME=$(cat $GLOBAL_APP_CONFIG | jq -r '.Solution')
+export APP_NAME=$(cat $GLOBAL_APP_CONFIG | jq -r '.App')
+
+currntPWD=`pwd`
+cd ${currntPWD}/source/
+
+echo ==--------CheckDedendencies---------==
+# npm install -g aws-cdk
+aws --version
+node --version
+npm --version
+cdk --version
+rush version
+pnpm --version
+git --version
+jq --version
+
+echo ==--------ConfigInfo---------==
+echo $GLOBAL_APP_CONFIG
+echo $AWS_PROFILE
+echo $PROJECT_STAGE
+echo $SOLUTION_NAME
+echo $APP_NAME
+echo .
+echo .
+
+echo ==--------BuildEnvironment---------==
+# rush purge
+rush install
+rush update
+rush rebuild
+
+echo .
+echo .
+
+echo ==--------PublishServicePackages---------==
+rush publish --apply --include-all --publish
+
+echo .
+echo .
+echo "Publishing Packages Completed"
+
+exit
diff --git a/source/.eslintrc.js b/source/.eslintrc.js
new file mode 100644
index 0000000..eb9baeb
--- /dev/null
+++ b/source/.eslintrc.js
@@ -0,0 +1,26 @@
+// This is a workaround for https://github.com/eslint/eslint/issues/3458
+// require('@rushstack/eslint-config/patch/modern-module-resolution');
+
+
+module.exports = {
+ root: true,
+ parser: "@typescript-eslint/parser",
+ plugins: [
+ "@typescript-eslint"
+ ],
+ extends: [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended"
+ ],
+ ignorePatterns: [ "**/dist", "**/build", "**/deploy", "**/node_modules", "**/*.spec.ts" ] ,
+ rules: {
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-empty-function": "off",
+ "no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
+ "no-prototype-builtins": "off",
+ "no-underscore-dangle": "off",
+ "no-unused-vars": "off"
+ }
+};
+
diff --git a/source/common/config/rush/.npmrc b/source/common/config/rush/.npmrc
new file mode 100644
index 0000000..ef0ca82
--- /dev/null
+++ b/source/common/config/rush/.npmrc
@@ -0,0 +1,2 @@
+registry=https://registry.npmjs.org/
+always-auth=false
diff --git a/source/common/config/rush/.npmrc-publish b/source/common/config/rush/.npmrc-publish
new file mode 100644
index 0000000..ef0ca82
--- /dev/null
+++ b/source/common/config/rush/.npmrc-publish
@@ -0,0 +1,2 @@
+registry=https://registry.npmjs.org/
+always-auth=false
diff --git a/source/common/config/rush/build-cache.json b/source/common/config/rush/build-cache.json
new file mode 100644
index 0000000..da74810
--- /dev/null
+++ b/source/common/config/rush/build-cache.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/build-cache.schema.json",
+ "buildCacheEnabled": false,
+ "cacheProvider": "local-only",
+ "azureBlobStorageConfiguration": {},
+ "amazonS3Configuration": {}
+}
\ No newline at end of file
diff --git a/source/common/config/rush/command-line.json b/source/common/config/rush/command-line.json
new file mode 100644
index 0000000..33b68eb
--- /dev/null
+++ b/source/common/config/rush/command-line.json
@@ -0,0 +1,63 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json",
+ "commands": [
+ {
+ "commandKind": "bulk",
+ "name": "clean",
+ "summary": "Clean build files.",
+ "description": "Removes any project specific build and deployment files from each project.",
+ "enableParallelism": true,
+ "allowWarningsInSuccessfulBuild": true
+ },
+ {
+ "commandKind": "bulk",
+ "name": "clean:postrelease",
+ "summary": "Clean build files.",
+ "description": "Removes any project specific build and deployment files from each project.",
+ "enableParallelism": true,
+ "allowWarningsInSuccessfulBuild": true
+ },
+ {
+ "commandKind": "bulk",
+ "name": "test",
+ "summary": "Tests projects.",
+ "description": "Runs unit tests for a project.",
+ "enableParallelism": true,
+ "allowWarningsInSuccessfulBuild": true,
+ "ignoreMissingScript": true
+ },
+ {
+ "commandKind": "bulk",
+ "name": "lint",
+ "summary": "Lints projects.",
+ "description": "Lints projects.",
+ "enableParallelism": true,
+ "ignoreMissingScript": true
+ },
+ {
+ "commandKind": "global",
+ "name": "commit",
+ "summary": "Git commit.",
+ "description": "Commits all staged files to the git repo.",
+ "safeForSimultaneousRushProcesses": false,
+ "shellCommand": "common/temp/node_modules/cz-customizable/standalone.js"
+ },
+ {
+ "commandKind": "global",
+ "name": "deploy-services",
+ "summary": "Deploy services for releasing",
+ "description": "",
+ "safeForSimultaneousRushProcesses": true,
+ "shellCommand": "common/scripts/deploy-services.bash"
+ },
+ {
+ "commandKind": "global",
+ "name": "destroy-services",
+ "summary": "Destroy services infrastructure",
+ "description": "",
+ "safeForSimultaneousRushProcesses": true,
+ "shellCommand": "common/scripts/destroy-services.bash"
+ }
+ ],
+ "parameters": []
+}
diff --git a/source/common/config/rush/common-versions.json b/source/common/config/rush/common-versions.json
new file mode 100644
index 0000000..66f385a
--- /dev/null
+++ b/source/common/config/rush/common-versions.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json",
+ "preferredVersions": {},
+ "allowedAlternativeVersions": {}
+}
diff --git a/source/common/config/rush/deploy.json b/source/common/config/rush/deploy.json
new file mode 100644
index 0000000..c6165de
--- /dev/null
+++ b/source/common/config/rush/deploy.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/deploy-scenario.schema.json",
+ "deploymentProjectNames": [
+ "@hanhdt/sample-service"
+ ],
+ "omitPnpmWorkaroundLinks": false,
+ "projectSettings": [
+ {
+ "projectName": "@hanhdt/sample-service",
+ "dependenciesToExclude": ["aws-sdk"]
+ }
+ ]
+}
diff --git a/source/common/config/rush/experiments.json b/source/common/config/rush/experiments.json
new file mode 100644
index 0000000..6cb06cd
--- /dev/null
+++ b/source/common/config/rush/experiments.json
@@ -0,0 +1,28 @@
+/**
+ * This configuration file allows repo maintainers to enable and disable experimental
+ * Rush features. For full documentation, please see https://rushjs.io
+ */
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/experiments.schema.json"
+
+ /**
+ * Rush 5.14.0 improved incremental builds to ignore spurious changes in the pnpm-lock.json file.
+ * This optimization is enabled by default. If you encounter a problem where "rush build" is neglecting
+ * to build some projects, please open a GitHub issue. As a workaround you can uncomment this line
+ * to temporarily restore the old behavior where everything must be rebuilt whenever pnpm-lock.json
+ * is modified.
+ */
+ // "legacyIncrementalBuildDependencyDetection": true,
+
+ /**
+ * By default, rush passes --no-prefer-frozen-lockfile to 'pnpm install'.
+ * Set this option to true to pass '--frozen-lockfile' instead.
+ */
+ // "usePnpmFrozenLockfileForRushInstall": true,
+
+ /**
+ * If true, the chmod field in temporary project tar headers will not be normalized.
+ * This normalization can help ensure consistent tarball integrity across platforms.
+ */
+ // "noChmodFieldInTarHeaderNormalization": true
+}
diff --git a/source/common/config/rush/version-policies.json b/source/common/config/rush/version-policies.json
new file mode 100644
index 0000000..ef0b4ba
--- /dev/null
+++ b/source/common/config/rush/version-policies.json
@@ -0,0 +1,90 @@
+/**
+ * This is configuration file is used for advanced publishing configurations with Rush.
+ * For full documentation, please see https://rushjs.io
+ */
+
+/**
+ * A list of version policy definitions. A "version policy" is a custom package versioning
+ * strategy that affects "rush change", "rush version", and "rush publish". The strategy applies
+ * to a set of projects that are specified using the "versionPolicyName" field in rush.json.
+ */
+[
+ // {
+ // /**
+ // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion").
+ // *
+ // * The "lockStepVersion" mode specifies that the projects will use "lock-step versioning". This
+ // * strategy is appropriate for a set of packages that act as selectable components of a
+ // * unified product. The entire set of packages are always published together, and always share
+ // * the same NPM version number. When the packages depend on other packages in the set, the
+ // * SemVer range is usually restricted to a single version.
+ // */
+ // "definitionName": "lockStepVersion",
+ //
+ // /**
+ // * (Required) The name that will be used for the "versionPolicyName" field in rush.json.
+ // * This name is also used command-line parameters such as "--version-policy"
+ // * and "--to-version-policy".
+ // */
+ // "policyName": "MyBigFramework",
+ //
+ // /**
+ // * (Required) The current version. All packages belonging to the set should have this version
+ // * in the current branch. When bumping versions, Rush uses this to determine the next version.
+ // * (The "version" field in package.json is NOT considered.)
+ // */
+ // "version": "1.0.0",
+ //
+ // /**
+ // * (Required) The type of bump that will be performed when publishing the next release.
+ // * When creating a release branch in Git, this field should be updated according to the
+ // * type of release.
+ // *
+ // * Valid values are: "prerelease", "release", "minor", "patch", "major"
+ // */
+ // "nextBump": "prerelease",
+ //
+ // /**
+ // * (Optional) If specified, all packages in the set share a common CHANGELOG.md file.
+ // * This file is stored with the specified "main" project, which must be a member of the set.
+ // *
+ // * If this field is omitted, then a separate CHANGELOG.md file will be maintained for each
+ // * package in the set.
+ // */
+ // "mainProject": "my-app"
+ // },
+ //
+ // {
+ // /**
+ // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion").
+ // *
+ // * The "individualVersion" mode specifies that the projects will use "individual versioning".
+ // * This is the typical NPM model where each package has an independent version number
+ // * and CHANGELOG.md file. Although a single CI definition is responsible for publishing the
+ // * packages, they otherwise don't have any special relationship. The version bumping will
+ // * depend on how developers answer the "rush change" questions for each package that
+ // * is changed.
+ // */
+ // "definitionName": "individualVersion",
+ //
+ // "policyName": "MyRandomLibraries",
+ //
+ // /**
+ // * (Optional) This can be used to enforce that all packages in the set must share a common
+ // * major version number, e.g. because they are from the same major release branch.
+ // * It can also be used to discourage people from accidentally making "MAJOR" SemVer changes
+ // * inappropriately. The minor/patch version parts will be bumped independently according
+ // * to the types of changes made to each project, according to the "rush change" command.
+ // */
+ // "lockedMajor": 3,
+ //
+ // /**
+ // * (Optional) When publishing is managed by Rush, by default the "rush change" command will
+ // * request changes for any projects that are modified by a pull request. These change entries
+ // * will produce a CHANGELOG.md file. If you author your CHANGELOG.md manually or announce updates
+ // * in some other way, set "exemptFromRushChange" to true to tell "rush change" to ignore the projects
+ // * belonging to this version policy.
+ // */
+ // "exemptFromRushChange": false
+ // }
+]
diff --git a/source/common/scripts/deploy-services.bash b/source/common/scripts/deploy-services.bash
new file mode 100755
index 0000000..653ced5
--- /dev/null
+++ b/source/common/scripts/deploy-services.bash
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+set -e
+if [[ "$DEBUG" == "true" ]]; then
+ set -x
+fi
+
+echo ==--------ConfigInfo---------==
+echo $GLOBAL_APP_CONFIG
+echo $PROJECT_STAGE
+echo .
+echo .
+
+cwd=$(dirname "$0")
+root_dir=$(pwd)
+
+deploy_setting_file="$root_dir/common/config/rush/deploy.json"
+json=$(cat $deploy_setting_file)
+projects_list=`echo $json | jq .deploymentProjectNames -r`
+services_array=$(echo $projects_list | jq -r -c '.[]')
+
+services_root="$root_dir/packages/services"
+for service in $services_array; do
+ arrIN=(${service//\// })
+ cd $services_root/${arrIN[1]}
+ export APP_CONFIG_DIR="$(pwd)/config"
+
+ if [[ $PROJECT_STAGE == "dev" || $PROJECT_STAGE == "development" ]];
+ then
+ export NODE_ENV=development
+ npm run deploy:dev
+ elif [[ $PROJECT_STAGE == "prod" || $PROJECT_STAGE == "production" ]];
+ then
+ export NODE_ENV=production
+ npm run deploy:prod
+ else
+ echo 'Please check your configuration file'
+ fi
+done
+
+exit
diff --git a/source/common/scripts/destroy-services.bash b/source/common/scripts/destroy-services.bash
new file mode 100755
index 0000000..d2dda4d
--- /dev/null
+++ b/source/common/scripts/destroy-services.bash
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+set -e
+if [[ "$DEBUG" == "true" ]]; then
+ set -x
+fi
+
+echo ==--------ConfigInfo---------==
+echo $APP_CONFIG
+echo $PROJECT_STAGE
+echo .
+echo .
+
+cwd=$(dirname "$0")
+root_dir=$(pwd)
+
+services_root="$root_dir/packages/services"
+for service in $(ls $services_root); do
+ cd "$services_root/$service"
+ export APP_CONFIG_DIR="$(pwd)/config"
+
+ if [[ $PROJECT_STAGE == "dev" || $PROJECT_STAGE == "development" ]];
+ then
+ export NODE_ENV=development
+ npm run destroy:dev
+ elif [[ $PROJECT_STAGE == "prod" || $PROJECT_STAGE == "production" ]];
+ then
+ export NODE_ENV=production
+ npm run destroy:prod
+ else
+ echo 'Please check your configuration file'
+ fi
+done
+
+exit
diff --git a/source/common/scripts/install-run-rush-pnpm.js b/source/common/scripts/install-run-rush-pnpm.js
new file mode 100644
index 0000000..5c14995
--- /dev/null
+++ b/source/common/scripts/install-run-rush-pnpm.js
@@ -0,0 +1,28 @@
+// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
+//
+// This script is intended for usage in an automated build environment where the Rush command may not have
+// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush
+// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the
+// rush-pnpm command.
+//
+// An example usage would be:
+//
+// node common/scripts/install-run-rush-pnpm.js pnpm-command
+//
+// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
+
+/******/ (() => { // webpackBootstrap
+/******/ "use strict";
+var __webpack_exports__ = {};
+/*!*****************************************************!*\
+ !*** ./lib-esnext/scripts/install-run-rush-pnpm.js ***!
+ \*****************************************************/
+
+// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
+// See the @microsoft/rush package's LICENSE file for license information.
+require('./install-run-rush');
+//# sourceMappingURL=install-run-rush-pnpm.js.map
+module.exports = __webpack_exports__;
+/******/ })()
+;
+//# sourceMappingURL=install-run-rush-pnpm.js.map
\ No newline at end of file
diff --git a/source/common/scripts/install-run-rush.js b/source/common/scripts/install-run-rush.js
new file mode 100644
index 0000000..cada1ed
--- /dev/null
+++ b/source/common/scripts/install-run-rush.js
@@ -0,0 +1,214 @@
+// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
+//
+// This script is intended for usage in an automated build environment where the Rush command may not have
+// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush
+// specified in the rush.json configuration file (if not already installed), and then pass a command-line to it.
+// An example usage would be:
+//
+// node common/scripts/install-run-rush.js install
+//
+// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
+
+/******/ (() => { // webpackBootstrap
+/******/ "use strict";
+/******/ var __webpack_modules__ = ({
+
+/***/ 657147:
+/*!*********************!*\
+ !*** external "fs" ***!
+ \*********************/
+/***/ ((module) => {
+
+module.exports = require("fs");
+
+/***/ }),
+
+/***/ 371017:
+/*!***********************!*\
+ !*** external "path" ***!
+ \***********************/
+/***/ ((module) => {
+
+module.exports = require("path");
+
+/***/ })
+
+/******/ });
+/************************************************************************/
+/******/ // The module cache
+/******/ var __webpack_module_cache__ = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/ // Check if module is in cache
+/******/ var cachedModule = __webpack_module_cache__[moduleId];
+/******/ if (cachedModule !== undefined) {
+/******/ return cachedModule.exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = __webpack_module_cache__[moduleId] = {
+/******/ // no module.id needed
+/******/ // no module.loaded needed
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/************************************************************************/
+/******/ /* webpack/runtime/compat get default export */
+/******/ (() => {
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = (module) => {
+/******/ var getter = module && module.__esModule ?
+/******/ () => (module['default']) :
+/******/ () => (module);
+/******/ __webpack_require__.d(getter, { a: getter });
+/******/ return getter;
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/define property getters */
+/******/ (() => {
+/******/ // define getter functions for harmony exports
+/******/ __webpack_require__.d = (exports, definition) => {
+/******/ for(var key in definition) {
+/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
+/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
+/******/ }
+/******/ }
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/hasOwnProperty shorthand */
+/******/ (() => {
+/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
+/******/ })();
+/******/
+/******/ /* webpack/runtime/make namespace object */
+/******/ (() => {
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = (exports) => {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/ })();
+/******/
+/************************************************************************/
+var __webpack_exports__ = {};
+// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
+(() => {
+/*!************************************************!*\
+ !*** ./lib-esnext/scripts/install-run-rush.js ***!
+ \************************************************/
+__webpack_require__.r(__webpack_exports__);
+/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! path */ 371017);
+/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__);
+/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 657147);
+/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__);
+// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
+// See the @microsoft/rush package's LICENSE file for license information.
+
+
+const { installAndRun, findRushJsonFolder, RUSH_JSON_FILENAME, runWithErrorAndStatusCode } = require('./install-run');
+const PACKAGE_NAME = '@microsoft/rush';
+const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION';
+const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_RUSH_LOCKFILE_PATH';
+function _getRushVersion(logger) {
+ const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION];
+ if (rushPreviewVersion !== undefined) {
+ logger.info(`Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}`);
+ return rushPreviewVersion;
+ }
+ const rushJsonFolder = findRushJsonFolder();
+ const rushJsonPath = path__WEBPACK_IMPORTED_MODULE_0__.join(rushJsonFolder, RUSH_JSON_FILENAME);
+ try {
+ const rushJsonContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(rushJsonPath, 'utf-8');
+ // Use a regular expression to parse out the rushVersion value because rush.json supports comments,
+ // but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script.
+ const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/);
+ return rushJsonMatches[1];
+ }
+ catch (e) {
+ throw new Error(`Unable to determine the required version of Rush from rush.json (${rushJsonFolder}). ` +
+ "The 'rushVersion' field is either not assigned in rush.json or was specified " +
+ 'using an unexpected syntax.');
+ }
+}
+function _getBin(scriptName) {
+ switch (scriptName.toLowerCase()) {
+ case 'install-run-rush-pnpm.js':
+ return 'rush-pnpm';
+ case 'install-run-rushx.js':
+ return 'rushx';
+ default:
+ return 'rush';
+ }
+}
+function _run() {
+ const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, ...packageBinArgs /* [build, --to, myproject] */] = process.argv;
+ // Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the
+ // appropriate binary inside the rush package to run
+ const scriptName = path__WEBPACK_IMPORTED_MODULE_0__.basename(scriptPath);
+ const bin = _getBin(scriptName);
+ if (!nodePath || !scriptPath) {
+ throw new Error('Unexpected exception: could not detect node path or script path');
+ }
+ let commandFound = false;
+ let logger = { info: console.log, error: console.error };
+ for (const arg of packageBinArgs) {
+ if (arg === '-q' || arg === '--quiet') {
+ // The -q/--quiet flag is supported by both `rush` and `rushx`, and will suppress
+ // any normal informational/diagnostic information printed during startup.
+ //
+ // To maintain the same user experience, the install-run* scripts pass along this
+ // flag but also use it to suppress any diagnostic information normally printed
+ // to stdout.
+ logger = {
+ info: () => { },
+ error: console.error
+ };
+ }
+ else if (!arg.startsWith('-') || arg === '-h' || arg === '--help') {
+ // We either found something that looks like a command (i.e. - doesn't start with a "-"),
+ // or we found the -h/--help flag, which can be run without a command
+ commandFound = true;
+ }
+ }
+ if (!commandFound) {
+ console.log(`Usage: ${scriptName} [args...]`);
+ if (scriptName === 'install-run-rush-pnpm.js') {
+ console.log(`Example: ${scriptName} pnpm-command`);
+ }
+ else if (scriptName === 'install-run-rush.js') {
+ console.log(`Example: ${scriptName} build --to myproject`);
+ }
+ else {
+ console.log(`Example: ${scriptName} custom-command`);
+ }
+ process.exit(1);
+ }
+ runWithErrorAndStatusCode(logger, () => {
+ const version = _getRushVersion(logger);
+ logger.info(`The rush.json configuration requests Rush version ${version}`);
+ const lockFilePath = process.env[INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE];
+ if (lockFilePath) {
+ logger.info(`Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}="${lockFilePath}", installing with lockfile.`);
+ }
+ return installAndRun(logger, PACKAGE_NAME, version, bin, packageBinArgs, lockFilePath);
+ });
+}
+_run();
+//# sourceMappingURL=install-run-rush.js.map
+})();
+
+module.exports = __webpack_exports__;
+/******/ })()
+;
+//# sourceMappingURL=install-run-rush.js.map
\ No newline at end of file
diff --git a/source/common/scripts/install-run-rushx.js b/source/common/scripts/install-run-rushx.js
new file mode 100644
index 0000000..b05df26
--- /dev/null
+++ b/source/common/scripts/install-run-rushx.js
@@ -0,0 +1,28 @@
+// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
+//
+// This script is intended for usage in an automated build environment where the Rush command may not have
+// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush
+// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the
+// rushx command.
+//
+// An example usage would be:
+//
+// node common/scripts/install-run-rushx.js custom-command
+//
+// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
+
+/******/ (() => { // webpackBootstrap
+/******/ "use strict";
+var __webpack_exports__ = {};
+/*!*************************************************!*\
+ !*** ./lib-esnext/scripts/install-run-rushx.js ***!
+ \*************************************************/
+
+// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
+// See the @microsoft/rush package's LICENSE file for license information.
+require('./install-run-rush');
+//# sourceMappingURL=install-run-rushx.js.map
+module.exports = __webpack_exports__;
+/******/ })()
+;
+//# sourceMappingURL=install-run-rushx.js.map
\ No newline at end of file
diff --git a/source/common/scripts/install-run.js b/source/common/scripts/install-run.js
new file mode 100644
index 0000000..68b1b56
--- /dev/null
+++ b/source/common/scripts/install-run.js
@@ -0,0 +1,645 @@
+// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
+//
+// This script is intended for usage in an automated build environment where a Node tool may not have
+// been preinstalled, or may have an unpredictable version. This script will automatically install the specified
+// version of the specified tool (if not already installed), and then pass a command-line to it.
+// An example usage would be:
+//
+// node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io
+//
+// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
+
+/******/ (() => { // webpackBootstrap
+/******/ "use strict";
+/******/ var __webpack_modules__ = ({
+
+/***/ 679877:
+/*!************************************************!*\
+ !*** ./lib-esnext/utilities/npmrcUtilities.js ***!
+ \************************************************/
+/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "syncNpmrc": () => (/* binding */ syncNpmrc)
+/* harmony export */ });
+/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! fs */ 657147);
+/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__);
+/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! path */ 371017);
+/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__);
+// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
+// See LICENSE in the project root for license information.
+// IMPORTANT - do not use any non-built-in libraries in this file
+
+
+/**
+ * As a workaround, copyAndTrimNpmrcFile() copies the .npmrc file to the target folder, and also trims
+ * unusable lines from the .npmrc file.
+ *
+ * Why are we trimming the .npmrc lines? NPM allows environment variables to be specified in
+ * the .npmrc file to provide different authentication tokens for different registry.
+ * However, if the environment variable is undefined, it expands to an empty string, which
+ * produces a valid-looking mapping with an invalid URL that causes an error. Instead,
+ * we'd prefer to skip that line and continue looking in other places such as the user's
+ * home directory.
+ *
+ * @returns
+ * The text of the the .npmrc.
+ */
+function _copyAndTrimNpmrcFile(logger, sourceNpmrcPath, targetNpmrcPath) {
+ logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose
+ logger.info(` --> "${targetNpmrcPath}"`);
+ let npmrcFileLines = fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(sourceNpmrcPath).toString().split('\n');
+ npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());
+ const resultLines = [];
+ // This finds environment variable tokens that look like "${VAR_NAME}"
+ const expansionRegExp = /\$\{([^\}]+)\}/g;
+ // Comment lines start with "#" or ";"
+ const commentRegExp = /^\s*[#;]/;
+ // Trim out lines that reference environment variables that aren't defined
+ for (const line of npmrcFileLines) {
+ let lineShouldBeTrimmed = false;
+ // Ignore comment lines
+ if (!commentRegExp.test(line)) {
+ const environmentVariables = line.match(expansionRegExp);
+ if (environmentVariables) {
+ for (const token of environmentVariables) {
+ // Remove the leading "${" and the trailing "}" from the token
+ const environmentVariableName = token.substring(2, token.length - 1);
+ // Is the environment variable defined?
+ if (!process.env[environmentVariableName]) {
+ // No, so trim this line
+ lineShouldBeTrimmed = true;
+ break;
+ }
+ }
+ }
+ }
+ if (lineShouldBeTrimmed) {
+ // Example output:
+ // "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}"
+ resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line);
+ }
+ else {
+ resultLines.push(line);
+ }
+ }
+ const combinedNpmrc = resultLines.join('\n');
+ fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(targetNpmrcPath, combinedNpmrc);
+ return combinedNpmrc;
+}
+/**
+ * syncNpmrc() copies the .npmrc file to the target folder, and also trims unusable lines from the .npmrc file.
+ * If the source .npmrc file not exist, then syncNpmrc() will delete an .npmrc that is found in the target folder.
+ *
+ * IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH Utilities._syncNpmrc()
+ *
+ * @returns
+ * The text of the the synced .npmrc, if one exists. If one does not exist, then undefined is returned.
+ */
+function syncNpmrc(sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish, logger = {
+ info: console.log,
+ error: console.error
+}) {
+ const sourceNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish');
+ const targetNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(targetNpmrcFolder, '.npmrc');
+ try {
+ if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) {
+ return _copyAndTrimNpmrcFile(logger, sourceNpmrcPath, targetNpmrcPath);
+ }
+ else if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcPath)) {
+ // If the source .npmrc doesn't exist and there is one in the target, delete the one in the target
+ logger.info(`Deleting ${targetNpmrcPath}`); // Verbose
+ fs__WEBPACK_IMPORTED_MODULE_0__.unlinkSync(targetNpmrcPath);
+ }
+ }
+ catch (e) {
+ throw new Error(`Error syncing .npmrc file: ${e}`);
+ }
+}
+//# sourceMappingURL=npmrcUtilities.js.map
+
+/***/ }),
+
+/***/ 532081:
+/*!********************************!*\
+ !*** external "child_process" ***!
+ \********************************/
+/***/ ((module) => {
+
+module.exports = require("child_process");
+
+/***/ }),
+
+/***/ 657147:
+/*!*********************!*\
+ !*** external "fs" ***!
+ \*********************/
+/***/ ((module) => {
+
+module.exports = require("fs");
+
+/***/ }),
+
+/***/ 822037:
+/*!*********************!*\
+ !*** external "os" ***!
+ \*********************/
+/***/ ((module) => {
+
+module.exports = require("os");
+
+/***/ }),
+
+/***/ 371017:
+/*!***********************!*\
+ !*** external "path" ***!
+ \***********************/
+/***/ ((module) => {
+
+module.exports = require("path");
+
+/***/ })
+
+/******/ });
+/************************************************************************/
+/******/ // The module cache
+/******/ var __webpack_module_cache__ = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/ // Check if module is in cache
+/******/ var cachedModule = __webpack_module_cache__[moduleId];
+/******/ if (cachedModule !== undefined) {
+/******/ return cachedModule.exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = __webpack_module_cache__[moduleId] = {
+/******/ // no module.id needed
+/******/ // no module.loaded needed
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/************************************************************************/
+/******/ /* webpack/runtime/compat get default export */
+/******/ (() => {
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = (module) => {
+/******/ var getter = module && module.__esModule ?
+/******/ () => (module['default']) :
+/******/ () => (module);
+/******/ __webpack_require__.d(getter, { a: getter });
+/******/ return getter;
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/define property getters */
+/******/ (() => {
+/******/ // define getter functions for harmony exports
+/******/ __webpack_require__.d = (exports, definition) => {
+/******/ for(var key in definition) {
+/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
+/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
+/******/ }
+/******/ }
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/hasOwnProperty shorthand */
+/******/ (() => {
+/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
+/******/ })();
+/******/
+/******/ /* webpack/runtime/make namespace object */
+/******/ (() => {
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = (exports) => {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/ })();
+/******/
+/************************************************************************/
+var __webpack_exports__ = {};
+// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
+(() => {
+/*!*******************************************!*\
+ !*** ./lib-esnext/scripts/install-run.js ***!
+ \*******************************************/
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "RUSH_JSON_FILENAME": () => (/* binding */ RUSH_JSON_FILENAME),
+/* harmony export */ "findRushJsonFolder": () => (/* binding */ findRushJsonFolder),
+/* harmony export */ "getNpmPath": () => (/* binding */ getNpmPath),
+/* harmony export */ "installAndRun": () => (/* binding */ installAndRun),
+/* harmony export */ "runWithErrorAndStatusCode": () => (/* binding */ runWithErrorAndStatusCode)
+/* harmony export */ });
+/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! child_process */ 532081);
+/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(child_process__WEBPACK_IMPORTED_MODULE_0__);
+/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 657147);
+/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__);
+/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! os */ 822037);
+/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(os__WEBPACK_IMPORTED_MODULE_2__);
+/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! path */ 371017);
+/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__);
+/* harmony import */ var _utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../utilities/npmrcUtilities */ 679877);
+// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
+// See the @microsoft/rush package's LICENSE file for license information.
+
+
+
+
+
+const RUSH_JSON_FILENAME = 'rush.json';
+const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER';
+const INSTALL_RUN_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_LOCKFILE_PATH';
+const INSTALLED_FLAG_FILENAME = 'installed.flag';
+const NODE_MODULES_FOLDER_NAME = 'node_modules';
+const PACKAGE_JSON_FILENAME = 'package.json';
+/**
+ * Parse a package specifier (in the form of name\@version) into name and version parts.
+ */
+function _parsePackageSpecifier(rawPackageSpecifier) {
+ rawPackageSpecifier = (rawPackageSpecifier || '').trim();
+ const separatorIndex = rawPackageSpecifier.lastIndexOf('@');
+ let name;
+ let version = undefined;
+ if (separatorIndex === 0) {
+ // The specifier starts with a scope and doesn't have a version specified
+ name = rawPackageSpecifier;
+ }
+ else if (separatorIndex === -1) {
+ // The specifier doesn't have a version
+ name = rawPackageSpecifier;
+ }
+ else {
+ name = rawPackageSpecifier.substring(0, separatorIndex);
+ version = rawPackageSpecifier.substring(separatorIndex + 1);
+ }
+ if (!name) {
+ throw new Error(`Invalid package specifier: ${rawPackageSpecifier}`);
+ }
+ return { name, version };
+}
+let _npmPath = undefined;
+/**
+ * Get the absolute path to the npm executable
+ */
+function getNpmPath() {
+ if (!_npmPath) {
+ try {
+ if (os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32') {
+ // We're on Windows
+ const whereOutput = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('where npm', { stdio: [] }).toString();
+ const lines = whereOutput.split(os__WEBPACK_IMPORTED_MODULE_2__.EOL).filter((line) => !!line);
+ // take the last result, we are looking for a .cmd command
+ // see https://github.com/microsoft/rushstack/issues/759
+ _npmPath = lines[lines.length - 1];
+ }
+ else {
+ // We aren't on Windows - assume we're on *NIX or Darwin
+ _npmPath = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('command -v npm', { stdio: [] }).toString();
+ }
+ }
+ catch (e) {
+ throw new Error(`Unable to determine the path to the NPM tool: ${e}`);
+ }
+ _npmPath = _npmPath.trim();
+ if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(_npmPath)) {
+ throw new Error('The NPM executable does not exist');
+ }
+ }
+ return _npmPath;
+}
+function _ensureFolder(folderPath) {
+ if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(folderPath)) {
+ const parentDir = path__WEBPACK_IMPORTED_MODULE_3__.dirname(folderPath);
+ _ensureFolder(parentDir);
+ fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(folderPath);
+ }
+}
+/**
+ * Create missing directories under the specified base directory, and return the resolved directory.
+ *
+ * Does not support "." or ".." path segments.
+ * Assumes the baseFolder exists.
+ */
+function _ensureAndJoinPath(baseFolder, ...pathSegments) {
+ let joinedPath = baseFolder;
+ try {
+ for (let pathSegment of pathSegments) {
+ pathSegment = pathSegment.replace(/[\\\/]/g, '+');
+ joinedPath = path__WEBPACK_IMPORTED_MODULE_3__.join(joinedPath, pathSegment);
+ if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(joinedPath)) {
+ fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(joinedPath);
+ }
+ }
+ }
+ catch (e) {
+ throw new Error(`Error building local installation folder (${path__WEBPACK_IMPORTED_MODULE_3__.join(baseFolder, ...pathSegments)}): ${e}`);
+ }
+ return joinedPath;
+}
+function _getRushTempFolder(rushCommonFolder) {
+ const rushTempFolder = process.env[RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME];
+ if (rushTempFolder !== undefined) {
+ _ensureFolder(rushTempFolder);
+ return rushTempFolder;
+ }
+ else {
+ return _ensureAndJoinPath(rushCommonFolder, 'temp');
+ }
+}
+/**
+ * Resolve a package specifier to a static version
+ */
+function _resolvePackageVersion(logger, rushCommonFolder, { name, version }) {
+ if (!version) {
+ version = '*'; // If no version is specified, use the latest version
+ }
+ if (version.match(/^[a-zA-Z0-9\-\+\.]+$/)) {
+ // If the version contains only characters that we recognize to be used in static version specifiers,
+ // pass the version through
+ return version;
+ }
+ else {
+ // version resolves to
+ try {
+ const rushTempFolder = _getRushTempFolder(rushCommonFolder);
+ const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush');
+ (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)(sourceNpmrcFolder, rushTempFolder, undefined, logger);
+ const npmPath = getNpmPath();
+ // This returns something that looks like:
+ // @microsoft/rush@3.0.0 '3.0.0'
+ // @microsoft/rush@3.0.1 '3.0.1'
+ // ...
+ // @microsoft/rush@3.0.20 '3.0.20'
+ //
+ const npmVersionSpawnResult = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(npmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier'], {
+ cwd: rushTempFolder,
+ stdio: []
+ });
+ if (npmVersionSpawnResult.status !== 0) {
+ throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`);
+ }
+ const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString();
+ const versionLines = npmViewVersionOutput.split('\n').filter((line) => !!line);
+ const latestVersion = versionLines[versionLines.length - 1];
+ if (!latestVersion) {
+ throw new Error('No versions found for the specified version range.');
+ }
+ const versionMatches = latestVersion.match(/^.+\s\'(.+)\'$/);
+ if (!versionMatches) {
+ throw new Error(`Invalid npm output ${latestVersion}`);
+ }
+ return versionMatches[1];
+ }
+ catch (e) {
+ throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`);
+ }
+ }
+}
+let _rushJsonFolder;
+/**
+ * Find the absolute path to the folder containing rush.json
+ */
+function findRushJsonFolder() {
+ if (!_rushJsonFolder) {
+ let basePath = __dirname;
+ let tempPath = __dirname;
+ do {
+ const testRushJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(basePath, RUSH_JSON_FILENAME);
+ if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(testRushJsonPath)) {
+ _rushJsonFolder = basePath;
+ break;
+ }
+ else {
+ basePath = tempPath;
+ }
+ } while (basePath !== (tempPath = path__WEBPACK_IMPORTED_MODULE_3__.dirname(basePath))); // Exit the loop when we hit the disk root
+ if (!_rushJsonFolder) {
+ throw new Error('Unable to find rush.json.');
+ }
+ }
+ return _rushJsonFolder;
+}
+/**
+ * Detects if the package in the specified directory is installed
+ */
+function _isPackageAlreadyInstalled(packageInstallFolder) {
+ try {
+ const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);
+ if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(flagFilePath)) {
+ return false;
+ }
+ const fileContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(flagFilePath).toString();
+ return fileContents.trim() === process.version;
+ }
+ catch (e) {
+ return false;
+ }
+}
+/**
+ * Delete a file. Fail silently if it does not exist.
+ */
+function _deleteFile(file) {
+ try {
+ fs__WEBPACK_IMPORTED_MODULE_1__.unlinkSync(file);
+ }
+ catch (err) {
+ if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') {
+ throw err;
+ }
+ }
+}
+/**
+ * Removes the following files and directories under the specified folder path:
+ * - installed.flag
+ * -
+ * - node_modules
+ */
+function _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath) {
+ try {
+ const flagFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME);
+ _deleteFile(flagFile);
+ const packageLockFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, 'package-lock.json');
+ if (lockFilePath) {
+ fs__WEBPACK_IMPORTED_MODULE_1__.copyFileSync(lockFilePath, packageLockFile);
+ }
+ else {
+ // Not running `npm ci`, so need to cleanup
+ _deleteFile(packageLockFile);
+ const nodeModulesFolder = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME);
+ if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(nodeModulesFolder)) {
+ const rushRecyclerFolder = _ensureAndJoinPath(rushTempFolder, 'rush-recycler');
+ fs__WEBPACK_IMPORTED_MODULE_1__.renameSync(nodeModulesFolder, path__WEBPACK_IMPORTED_MODULE_3__.join(rushRecyclerFolder, `install-run-${Date.now().toString()}`));
+ }
+ }
+ }
+ catch (e) {
+ throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`);
+ }
+}
+function _createPackageJson(packageInstallFolder, name, version) {
+ try {
+ const packageJsonContents = {
+ name: 'ci-rush',
+ version: '0.0.0',
+ dependencies: {
+ [name]: version
+ },
+ description: "DON'T WARN",
+ repository: "DON'T WARN",
+ license: 'MIT'
+ };
+ const packageJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, PACKAGE_JSON_FILENAME);
+ fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContents, undefined, 2));
+ }
+ catch (e) {
+ throw new Error(`Unable to create package.json: ${e}`);
+ }
+}
+/**
+ * Run "npm install" in the package install folder.
+ */
+function _installPackage(logger, packageInstallFolder, name, version, command) {
+ try {
+ logger.info(`Installing ${name}...`);
+ const npmPath = getNpmPath();
+ const result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(npmPath, [command], {
+ stdio: 'inherit',
+ cwd: packageInstallFolder,
+ env: process.env
+ });
+ if (result.status !== 0) {
+ throw new Error(`"npm ${command}" encountered an error`);
+ }
+ logger.info(`Successfully installed ${name}@${version}`);
+ }
+ catch (e) {
+ throw new Error(`Unable to install package: ${e}`);
+ }
+}
+/**
+ * Get the ".bin" path for the package.
+ */
+function _getBinPath(packageInstallFolder, binName) {
+ const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin');
+ const resolvedBinName = os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32' ? `${binName}.cmd` : binName;
+ return path__WEBPACK_IMPORTED_MODULE_3__.resolve(binFolderPath, resolvedBinName);
+}
+/**
+ * Write a flag file to the package's install directory, signifying that the install was successful.
+ */
+function _writeFlagFile(packageInstallFolder) {
+ try {
+ const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);
+ fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(flagFilePath, process.version);
+ }
+ catch (e) {
+ throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`);
+ }
+}
+function installAndRun(logger, packageName, packageVersion, packageBinName, packageBinArgs, lockFilePath = process.env[INSTALL_RUN_LOCKFILE_PATH_VARIABLE]) {
+ const rushJsonFolder = findRushJsonFolder();
+ const rushCommonFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushJsonFolder, 'common');
+ const rushTempFolder = _getRushTempFolder(rushCommonFolder);
+ const packageInstallFolder = _ensureAndJoinPath(rushTempFolder, 'install-run', `${packageName}@${packageVersion}`);
+ if (!_isPackageAlreadyInstalled(packageInstallFolder)) {
+ // The package isn't already installed
+ _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath);
+ const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush');
+ (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)(sourceNpmrcFolder, packageInstallFolder, undefined, logger);
+ _createPackageJson(packageInstallFolder, packageName, packageVersion);
+ const command = lockFilePath ? 'ci' : 'install';
+ _installPackage(logger, packageInstallFolder, packageName, packageVersion, command);
+ _writeFlagFile(packageInstallFolder);
+ }
+ const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`;
+ const statusMessageLine = new Array(statusMessage.length + 1).join('-');
+ logger.info('\n' + statusMessage + '\n' + statusMessageLine + '\n');
+ const binPath = _getBinPath(packageInstallFolder, packageBinName);
+ const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin');
+ // Windows environment variables are case-insensitive. Instead of using SpawnSyncOptions.env, we need to
+ // assign via the process.env proxy to ensure that we append to the right PATH key.
+ const originalEnvPath = process.env.PATH || '';
+ let result;
+ try {
+ // Node.js on Windows can not spawn a file when the path has a space on it
+ // unless the path gets wrapped in a cmd friendly way and shell mode is used
+ const shouldUseShell = binPath.includes(' ') && os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32';
+ const platformBinPath = shouldUseShell ? `"${binPath}"` : binPath;
+ process.env.PATH = [binFolderPath, originalEnvPath].join(path__WEBPACK_IMPORTED_MODULE_3__.delimiter);
+ result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformBinPath, packageBinArgs, {
+ stdio: 'inherit',
+ windowsVerbatimArguments: false,
+ shell: shouldUseShell,
+ cwd: process.cwd(),
+ env: process.env
+ });
+ }
+ finally {
+ process.env.PATH = originalEnvPath;
+ }
+ if (result.status !== null) {
+ return result.status;
+ }
+ else {
+ throw result.error || new Error('An unknown error occurred.');
+ }
+}
+function runWithErrorAndStatusCode(logger, fn) {
+ process.exitCode = 1;
+ try {
+ const exitCode = fn();
+ process.exitCode = exitCode;
+ }
+ catch (e) {
+ logger.error('\n\n' + e.toString() + '\n\n');
+ }
+}
+function _run() {
+ const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, rawPackageSpecifier /* qrcode@^1.2.0 */, packageBinName /* qrcode */, ...packageBinArgs /* [-f, myproject/lib] */] = process.argv;
+ if (!nodePath) {
+ throw new Error('Unexpected exception: could not detect node path');
+ }
+ if (path__WEBPACK_IMPORTED_MODULE_3__.basename(scriptPath).toLowerCase() !== 'install-run.js') {
+ // If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control
+ // to the script that (presumably) imported this file
+ return;
+ }
+ if (process.argv.length < 4) {
+ console.log('Usage: install-run.js @ [args...]');
+ console.log('Example: install-run.js qrcode@1.2.2 qrcode https://rushjs.io');
+ process.exit(1);
+ }
+ const logger = { info: console.log, error: console.error };
+ runWithErrorAndStatusCode(logger, () => {
+ const rushJsonFolder = findRushJsonFolder();
+ const rushCommonFolder = _ensureAndJoinPath(rushJsonFolder, 'common');
+ const packageSpecifier = _parsePackageSpecifier(rawPackageSpecifier);
+ const name = packageSpecifier.name;
+ const version = _resolvePackageVersion(logger, rushCommonFolder, packageSpecifier);
+ if (packageSpecifier.version !== version) {
+ console.log(`Resolved to ${name}@${version}`);
+ }
+ return installAndRun(logger, name, version, packageBinName, packageBinArgs);
+ });
+}
+_run();
+//# sourceMappingURL=install-run.js.map
+})();
+
+module.exports = __webpack_exports__;
+/******/ })()
+;
+//# sourceMappingURL=install-run.js.map
\ No newline at end of file
diff --git a/source/package.json b/source/package.json
new file mode 100644
index 0000000..89a73aa
--- /dev/null
+++ b/source/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "ts-microservices-rush-boilerplate",
+ "author": "Hanh Tran",
+ "scripts": {},
+ "engines": {
+ "node": "=18",
+ "pnpm": "=6"
+ },
+ "private": true,
+ "husky": {
+ "hooks": {
+ "pre-commit": "rush lint",
+ "prepare-commit-msg": "exec < /dev/tty && git cz --hook || true",
+ "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
+ }
+ }
+}
diff --git a/source/packages/libraries/core/config-inject/.npmignore b/source/packages/libraries/core/config-inject/.npmignore
new file mode 100644
index 0000000..35d8969
--- /dev/null
+++ b/source/packages/libraries/core/config-inject/.npmignore
@@ -0,0 +1,4 @@
+.nyc_output
+.rush
+*.log
+tsconfig.tsbuildinfo
\ No newline at end of file
diff --git a/source/packages/libraries/core/config-inject/package.json b/source/packages/libraries/core/config-inject/package.json
new file mode 100644
index 0000000..5f2af2c
--- /dev/null
+++ b/source/packages/libraries/core/config-inject/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "@hanhdt/config-inject",
+ "version": "1.0.0",
+ "description": "This library load config from files and inject it into process.env to make it accessible as environment variables",
+ "author": "Hanh Tran ",
+ "main": "dist/index.js",
+ "typings": "dist/index.d.ts",
+ "scripts": {
+ "clean": "npx shx rm -rf dist deploy tsconfig.tsbuildinfo bundle.zip .rush .nyc_output *.log",
+ "lint": "npx eslint . --ext '.ts'",
+ "build": "npx tsc -b",
+ "test": "rushx lint && APP_CONFIG_DIR='config' jest --silent --passWithNoTests"
+ },
+ "dependencies": {
+ "@hanhdt/logger": "^1.0.0",
+ "dotenv-flow": "^3.2.0",
+ "dotenv": "^8.2.0"
+ },
+ "devDependencies": {
+ "@rushstack/eslint-config": "2.3.4",
+ "eslint": "7.26.0",
+ "nyc": "15.1.0",
+ "shx": "0.3.3",
+ "@types/jest": "26.0.23",
+ "@typescript-eslint/eslint-plugin": "4.23.0",
+ "@typescript-eslint/parser": "4.23.0",
+ "jest-create-mock-instance": "1.1.0",
+ "jest-haste-map": "26.6.2",
+ "jest-mock-extended": "1.0.14",
+ "jest-mock": "26.6.2",
+ "jest-resolve": "26.6.2",
+ "jest": "26.6.3",
+ "ts-jest": "26.5.6",
+ "typescript": "4.2.4",
+ "@types/node": "16.11.7",
+ "@types/dotenv-flow": "~3.2.0"
+ },
+ "jest": {
+ "globals": {},
+ "roots": [
+ "/src"
+ ],
+ "transform": {
+ "^.+\\.tsx?$": "ts-jest"
+ },
+ "testMatch": [
+ "/**/?(*.)+(spec|test).ts?(x)"
+ ],
+ "moduleFileExtensions": [
+ "ts",
+ "tsx",
+ "js",
+ "jsx",
+ "json",
+ "node"
+ ]
+ },
+ "license": "ISC"
+}
\ No newline at end of file
diff --git a/source/packages/libraries/core/config-inject/src/index.ts b/source/packages/libraries/core/config-inject/src/index.ts
new file mode 100644
index 0000000..137acbf
--- /dev/null
+++ b/source/packages/libraries/core/config-inject/src/index.ts
@@ -0,0 +1,35 @@
+import { config } from 'dotenv-flow';
+import dotenv from 'dotenv';
+
+// APP_CONFIG contains list of environment variables
+// This will be loaded first
+if (process.env.APP_CONFIG) {
+ const result = dotenv.parse(process.env.APP_CONFIG)
+ console.log(`Loaded from APP_CONFIG: ${JSON.stringify(result)}`)
+ Object.assign(process.env, result)
+}
+
+// APP_CONFIG_DIR is specified in cloudformation definition of lambda and npm run start of the services
+// This will populate any value that is not specified by APP_CONFIG with default value (dotenv.load functionality)
+const fileLocations = [
+ process.env.APP_CONFIG_DIR + '/.env.defaults',
+ process.env.APP_CONFIG_DIR + '/.env.local',
+ process.env.APP_CONFIG_DIR + '/.env.development',
+ process.env.APP_CONFIG_DIR + '/.env.production'
+];
+
+const config_location: string = process.env.CONFIG_LOCATION !== undefined ? process.env.CONFIG_LOCATION : '';
+if ((config_location.length ?? 0) > 0) {
+ console.log(`Loading config from ${config_location}`)
+ fileLocations.push(config_location);
+}
+
+// load(fileLocations);
+config({
+ node_env: process.env.NODE_ENV || 'development',
+ default_node_env: 'development',
+ path: process.env.APP_CONFIG_DIR,
+});
+
+console.log(`Module config-inject loaded config:`);
+console.log(process.env);
\ No newline at end of file
diff --git a/source/packages/libraries/core/config-inject/tsconfig.json b/source/packages/libraries/core/config-inject/tsconfig.json
new file mode 100644
index 0000000..5e30378
--- /dev/null
+++ b/source/packages/libraries/core/config-inject/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.libraries.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "references": [{
+ "path": "../../core/logger"
+ }],
+ "include": [
+ "src"
+ ]
+}
\ No newline at end of file
diff --git a/source/packages/libraries/core/logger/.npmignore b/source/packages/libraries/core/logger/.npmignore
new file mode 100644
index 0000000..cfe7129
--- /dev/null
+++ b/source/packages/libraries/core/logger/.npmignore
@@ -0,0 +1,4 @@
+.nyc_output
+.rush
+*.log
+tsconfig.tsbuildinfo
diff --git a/source/packages/libraries/core/logger/package.json b/source/packages/libraries/core/logger/package.json
new file mode 100644
index 0000000..cbd23c3
--- /dev/null
+++ b/source/packages/libraries/core/logger/package.json
@@ -0,0 +1,60 @@
+{
+ "name": "@hanhdt/logger",
+ "description": "Common logging capability for platform.",
+ "version": "1.0.0",
+ "author": "Hanh Tran ",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "scripts": {
+ "clean": "npx shx rm -rf dist tsconfig.tsbuildinfo bundle.zip .rush .nyc_output *.log",
+ "lint": "npx eslint . --ext '.ts'",
+ "build": "npx tsc -b",
+ "test": "rushx lint && jest --silent --passWithNoTests"
+ },
+ "dependencies": {
+ "logform": "^2.2.0",
+ "readable-stream": "^3.6.0",
+ "winston": "^3.3.3",
+ "winston-transport": "^4.4.0"
+ },
+ "devDependencies": {
+ "@rushstack/eslint-config": "2.3.4",
+ "eslint": "7.26.0",
+ "nyc": "15.1.0",
+ "shx": "0.3.3",
+ "@types/jest": "26.0.23",
+ "@typescript-eslint/eslint-plugin": "4.23.0",
+ "@typescript-eslint/parser": "4.23.0",
+ "jest-create-mock-instance": "1.1.0",
+ "jest-haste-map": "26.6.2",
+ "jest-mock-extended": "1.0.14",
+ "jest-mock": "26.6.2",
+ "jest-resolve": "26.6.2",
+ "jest": "26.6.3",
+ "ts-jest": "26.5.6",
+ "typescript": "4.2.4"
+ },
+ "jest": {
+ "globals": {
+ },
+ "roots": [
+ "/src"
+ ],
+ "transform": {
+ "^.+\\.tsx?$": "ts-jest"
+ },
+ "testMatch": [
+ "/**/?(*.)+(spec|test).ts?(x)"
+ ],
+ "moduleFileExtensions": [
+ "ts",
+ "tsx",
+ "js",
+ "jsx",
+ "json",
+ "node"
+ ]
+ },
+ "license": "ISC",
+ "private": true
+}
diff --git a/source/packages/libraries/core/logger/src/index.ts b/source/packages/libraries/core/logger/src/index.ts
new file mode 100644
index 0000000..ecc7c61
--- /dev/null
+++ b/source/packages/libraries/core/logger/src/index.ts
@@ -0,0 +1,62 @@
+import { createLogger, Logger, LoggerOptions, transports } from 'winston';
+
+/**
+ * Class representing Logging mechanism.
+ * @type {CDFLogger}
+ * @module CDFLogger
+ */
+export class CDFLogger {
+ private readonly _internalLogger: Logger;
+
+ /**
+ * Construct new instance of logger
+ * @param logLevel of the logger
+ */
+ constructor(logLevel?: string) {
+ const defaultLoggingOptions: LoggerOptions = {
+ level: logLevel ? logLevel : 'debug',
+ exitOnError: false,
+ transports: [
+ new transports.Console()
+ ]
+ };
+ this._internalLogger = createLogger(defaultLoggingOptions);
+ }
+
+ /**
+ * Log at debug level
+ * @param message to be logged
+ * @param meta any other objects to be logged
+ */
+ public debug(message: string, ...meta: any[]): void {
+ this._internalLogger.debug(message, meta);
+ }
+
+ /**
+ * Log at info level
+ * @param message to be logged
+ * @param meta any other objects to be logged
+ */
+ public info(message: string, ...meta: any[]): void {
+ this._internalLogger.info(message, meta);
+ }
+
+ /**
+ * Log at warn level
+ * @param message to be logged
+ * @param meta any other objects to be logged
+ */
+ public warn(message: string, ...meta: any[]): void {
+ this._internalLogger.warn(message, meta);
+ }
+
+ /**
+ * Log at error level
+ * @param message to be logged
+ * @param meta any other objects to be logged
+ */
+ public error(message: string, ...meta: any[]): void {
+ this._internalLogger.error(message, meta);
+ }
+
+}
diff --git a/source/packages/libraries/core/logger/tsconfig.json b/source/packages/libraries/core/logger/tsconfig.json
new file mode 100644
index 0000000..2c2a2ff
--- /dev/null
+++ b/source/packages/libraries/core/logger/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../tsconfig.libraries.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "include": [
+ "src"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist"
+ ]
+}
\ No newline at end of file
diff --git a/source/packages/libraries/tsconfig.libraries.json b/source/packages/libraries/tsconfig.libraries.json
new file mode 100644
index 0000000..0510ede
--- /dev/null
+++ b/source/packages/libraries/tsconfig.libraries.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true,
+ "experimentalDecorators": true
+ },
+ "exclude": [
+ "node_modules",
+ "dist",
+ ".vscode",
+ ".git",
+ "./**/__mocks__/*.ts"
+ ]
+}
\ No newline at end of file
diff --git a/source/packages/services/sample-service/.gitignore b/source/packages/services/sample-service/.gitignore
new file mode 100644
index 0000000..05cfb00
--- /dev/null
+++ b/source/packages/services/sample-service/.gitignore
@@ -0,0 +1,19 @@
+node_modules
+*.js
+*.d.ts
+*.js.map
+*.d.ts.map
+
+# CDK asset staging directory
+.cdk.staging
+cdk.out
+
+# Coverage directory used by tools like istanbul
+coverage
+.nyc_output
+.env.*
+.env
+
+!jest.config.js
+!cucumber.js
+!.env.sample
\ No newline at end of file
diff --git a/source/packages/services/sample-service/.npmignore b/source/packages/services/sample-service/.npmignore
new file mode 100644
index 0000000..c1d6d45
--- /dev/null
+++ b/source/packages/services/sample-service/.npmignore
@@ -0,0 +1,6 @@
+*.ts
+!*.d.ts
+
+# CDK asset staging directory
+.cdk.staging
+cdk.out
diff --git a/source/packages/services/sample-service/README.md b/source/packages/services/sample-service/README.md
new file mode 100644
index 0000000..1ddc85b
--- /dev/null
+++ b/source/packages/services/sample-service/README.md
@@ -0,0 +1,24 @@
+# Welcome to your CDK TypeScript project
+
+You should explore the contents of this project. It demonstrates a CDK app with an instance of a stack (`p2-dashboard-cdf-[env]-canframesRestApi-stack`)
+
+The `cdk.json` file tells the CDK Toolkit how to execute your app.
+
+## Share bootstrapping with AWS CDK Toolkit
+
+```bash
+ $cdk bootstrap aws:///
+```
+
+## Useful commands
+
+* `npm run build` compile typescript to js
+* `npm run watch` watch for changes and compile
+* `npm run test` perform the BDD tests with Cucumber.js
+* `npm run deploy` deploy this stack to your default AWS account/region
+* `npm run cdk diff` compare deployed stack with current state
+* `npm run cdk synth` emits the synthesized CloudFormation template
+
+## Test framework
+
+We are using Cucumber.js for BDD testing. The tests are located in the `test` folder.
diff --git a/source/packages/services/sample-service/cdk.json b/source/packages/services/sample-service/cdk.json
new file mode 100644
index 0000000..10d1208
--- /dev/null
+++ b/source/packages/services/sample-service/cdk.json
@@ -0,0 +1,51 @@
+{
+ "app": "npx ts-node --prefer-ts-exts infrastructure/canframesApp.ts",
+ "watch": {
+ "include": [
+ "**"
+ ],
+ "exclude": [
+ "README.md",
+ "cdk*.json",
+ "**/*.d.ts",
+ "**/*.js",
+ "tsconfig.json",
+ "package*.json",
+ "yarn.lock",
+ "node_modules",
+ "test"
+ ]
+ },
+ "context": {
+ "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
+ "@aws-cdk/core:checkSecretUsage": true,
+ "@aws-cdk/core:target-partitions": [
+ "aws",
+ "aws-cn"
+ ],
+ "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
+ "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
+ "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
+ "@aws-cdk/aws-iam:minimizePolicies": true,
+ "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
+ "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
+ "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
+ "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
+ "@aws-cdk/core:enablePartitionLiterals": true,
+ "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
+ "@aws-cdk/aws-iam:standardizedServicePrincipals": true,
+ "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
+ "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
+ "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
+ "@aws-cdk/aws-route53-patters:useCertificate": true,
+ "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
+ "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
+ "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
+ "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
+ "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
+ "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
+ "@aws-cdk/aws-redshift:columnId": true,
+ "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true
+ }
+}
diff --git a/source/packages/services/sample-service/config/.env.sample b/source/packages/services/sample-service/config/.env.sample
new file mode 100644
index 0000000..aa1fc52
--- /dev/null
+++ b/source/packages/services/sample-service/config/.env.sample
@@ -0,0 +1 @@
+LogLevel=info
\ No newline at end of file
diff --git a/source/packages/services/sample-service/cucumber.js b/source/packages/services/sample-service/cucumber.js
new file mode 100644
index 0000000..d16cff0
--- /dev/null
+++ b/source/packages/services/sample-service/cucumber.js
@@ -0,0 +1,12 @@
+let common = [
+ 'test/cucumber/features/**/*.feature', // Specify our feature files
+ '--publish-quiet', // disable this message
+ '--require-module ts-node/register', // Load TypeScript module
+ '--require test/cucumber/step-definitions/**/*.ts', // Load step definitions
+ '--format @cucumber/pretty-formatter', // Load custom formatter
+ '--format-options {"theme":{"feature keyword":["magenta","bold"],"feature name":["bold"],"scenario keyword":["magenta","bold"],"scenario name":["bold"],"step keyword":["green","bold"],"step text":["greenBright","italic"],"tag":["green"]}}'
+].join(' ');
+
+module.exports = {
+ default: common
+};
\ No newline at end of file
diff --git a/source/packages/services/sample-service/infrastructure/buildConfig.ts b/source/packages/services/sample-service/infrastructure/buildConfig.ts
new file mode 100644
index 0000000..e2094f8
--- /dev/null
+++ b/source/packages/services/sample-service/infrastructure/buildConfig.ts
@@ -0,0 +1,21 @@
+export interface BuildConfig {
+ readonly AWSAccountID: string;
+ readonly AWSProfileName: string;
+ readonly AWSProfileRegion: string;
+
+ readonly App: string;
+ readonly Environment: string;
+ readonly Solution: string;
+ readonly Version: string;
+ readonly Build: string;
+
+ readonly Parameters: BuildParameters;
+}
+
+export interface BuildParameters {
+ readonly LogLevel: string;
+ readonly LambdaInsightsLayer: string;
+ readonly ExternalApiUrl: string;
+
+ readonly CanframesTableName: string;
+}
\ No newline at end of file
diff --git a/source/packages/services/sample-service/infrastructure/canframesApp.ts b/source/packages/services/sample-service/infrastructure/canframesApp.ts
new file mode 100644
index 0000000..b614fec
--- /dev/null
+++ b/source/packages/services/sample-service/infrastructure/canframesApp.ts
@@ -0,0 +1,72 @@
+#!/usr/bin/env node
+import * as cdk from 'aws-cdk-lib';
+import path from 'path';
+import '@hanhdt/config-inject';
+import { CanframesRestApiStack } from './canframesRestApiStack';
+import { BuildConfig } from './buildConfig';
+import { name } from '../package.json';
+
+const app = new cdk.App();
+
+function ensureString(object: { [name: string]: any }, propName: string): string {
+ if (process.env[propName]) {
+ return process.env[propName] || '';
+ }
+
+ if (!object[propName] || object[propName].trim().length === 0) {
+ process.emitWarning(`${propName} does not exist or is empty globally`);
+ }
+
+ return object[propName];
+}
+
+function getConfig(): BuildConfig {
+ const env = app.node.tryGetContext('config');
+ if (!env) {
+ throw new Error('Context variable missing on CDK command. Please pass in as -c config=');
+ }
+
+ const appConfigJSON = require(path.resolve(`../../../../config/app-config-${env}.json`));
+ const unparsedEnv = appConfigJSON || app.node.tryGetContext(env);
+
+ const buildConfig: BuildConfig = {
+ AWSAccountID: ensureString(unparsedEnv, 'AWSAccountID'),
+ AWSProfileName: ensureString(unparsedEnv, 'AWSProfileName'),
+ AWSProfileRegion: ensureString(unparsedEnv, 'AWSProfileRegion'),
+
+ Solution: ensureString(unparsedEnv, 'Solution'),
+ Environment: ensureString(unparsedEnv, 'Environment'),
+ App: ensureString(unparsedEnv, 'App'),
+ Version: ensureString(unparsedEnv, 'Version'),
+ Build: ensureString(unparsedEnv, 'Build'),
+
+ Parameters: {
+ LogLevel: ensureString(unparsedEnv, 'LogLevel'),
+ LambdaInsightsLayer: ensureString(unparsedEnv, 'LambdaInsightsLayer'),
+ ExternalApiUrl: ensureString(unparsedEnv, 'ExternalApiUrl'),
+ CanframesTableName: ensureString(unparsedEnv, 'CanframesTableName'),
+ }
+ };
+
+ return buildConfig;
+}
+
+async function main() {
+ const buildConfig: BuildConfig = getConfig();
+
+ const serviceName = name.replace('@', '').replace('/', '-');
+ const canframesRestApiStackName = `${buildConfig.Solution}-${buildConfig.App}-${serviceName}-stack-${buildConfig.Environment}`;
+
+ cdk.Tags.of(app).add('App', `${buildConfig.Solution}-${buildConfig.App}`);
+ cdk.Tags.of(app).add('Environment', buildConfig.Environment);
+ cdk.Tags.of(app).add('Service', serviceName);
+
+ new CanframesRestApiStack(app, canframesRestApiStackName, {
+ env: {
+ region: buildConfig.AWSProfileRegion,
+ account: buildConfig.AWSAccountID,
+ },
+ }, buildConfig);
+}
+
+main();
\ No newline at end of file
diff --git a/source/packages/services/sample-service/infrastructure/canframesRestApiStack.ts b/source/packages/services/sample-service/infrastructure/canframesRestApiStack.ts
new file mode 100644
index 0000000..96e0f53
--- /dev/null
+++ b/source/packages/services/sample-service/infrastructure/canframesRestApiStack.ts
@@ -0,0 +1,157 @@
+import { Stack, StackProps, Duration, RemovalPolicy } from 'aws-cdk-lib';
+import { Function, Runtime } from 'aws-cdk-lib/aws-lambda';
+import { RestApi, LambdaIntegration, Resource } from 'aws-cdk-lib/aws-apigateway';
+import * as ddb from 'aws-cdk-lib/aws-dynamodb';
+import { Construct } from 'constructs';
+import { TypeScriptCode } from "@mrgrain/cdk-esbuild";
+import { BuildConfig } from './buildConfig';
+
+export class CanframesRestApiStack extends Stack {
+ private restAPI: RestApi;
+ private canframesResource: Resource;
+ private getCanframesFunction: Function;
+ private getCanframeFunction: Function;
+ private createCanframeFunction: Function;
+ private updateCanframeFunction: Function;
+ private deleteCanframeFunction: Function;
+
+
+ constructor(scope: Construct, id: string, props: StackProps, buildConfig: BuildConfig) {
+ super(scope, id, props);
+
+ // crate a dynamodb table
+ const canframesTableName = buildConfig.Parameters.CanframesTableName;
+ const canframesTable = new ddb.Table(this, 'CanframesApiTable', {
+ tableName: canframesTableName,
+ partitionKey: { name: 'id', type: ddb.AttributeType.STRING },
+ sortKey: { name: 'year', type: ddb.AttributeType.STRING },
+ removalPolicy: RemovalPolicy.DESTROY
+ })
+
+ const getCanframesCode = new TypeScriptCode('src/getCanframesHandler.ts', {
+ buildOptions: {
+ bundle: true,
+ treeShaking: true,
+ external: ['aws-sdk'],
+ },
+ });
+
+ const getCanframeCode = new TypeScriptCode('src/getCanframeHandler.ts', {
+ buildOptions: {
+ bundle: true,
+ treeShaking: true,
+ external: ['aws-sdk'],
+ }
+ });
+
+ const createCanframeCode = new TypeScriptCode('src/createCanframeHandler.ts', {
+ buildOptions: {
+ bundle: true,
+ treeShaking: true,
+ external: ['aws-sdk'],
+ }
+ });
+
+ const updateCanframeCode = new TypeScriptCode('src/updateCanframeHandler.ts', {
+ buildOptions: {
+ bundle: true,
+ treeShaking: true,
+ external: ['aws-sdk'],
+ }
+ });
+
+ const deleteCanframeCode = new TypeScriptCode('src/deleteCanframeHandler.ts', {
+ buildOptions: {
+ bundle: true,
+ treeShaking: true,
+ external: ['aws-sdk'],
+ }
+ });
+
+ this.getCanframesFunction = new Function(this, 'getCanframes', {
+ functionName: `${buildConfig.Solution}-${buildConfig.App}-getCanframes-${buildConfig.Environment}`,
+ runtime: Runtime.NODEJS_16_X,
+ code: getCanframesCode,
+ handler: 'getCanframesHandler.handler',
+ memorySize: 512,
+ timeout: Duration.seconds(10),
+ environment: {
+ LOG_LEVEL: buildConfig.Parameters.LogLevel,
+ DYNAMODB_TABLE_CANFRAMES: buildConfig.Parameters.CanframesTableName,
+ }
+ });
+
+ this.getCanframeFunction = new Function(this, 'getCanframe', {
+ functionName: `${buildConfig.Solution}-${buildConfig.App}-getCanframe-${buildConfig.Environment}`,
+ runtime: Runtime.NODEJS_16_X,
+ code: getCanframeCode,
+ handler: 'getCanframeHandler.handler',
+ memorySize: 512,
+ timeout: Duration.seconds(10),
+ environment: {
+ LOG_LEVEL: buildConfig.Parameters.LogLevel,
+ DYNAMODB_TABLE_CANFRAMES: buildConfig.Parameters.CanframesTableName,
+ }
+ });
+
+ this.createCanframeFunction = new Function(this, 'createCanframe', {
+ functionName: `${buildConfig.Solution}-${buildConfig.App}-createCanframe-${buildConfig.Environment}`,
+ runtime: Runtime.NODEJS_16_X,
+ code: createCanframeCode,
+ handler: 'createCanframeHandler.handler',
+ memorySize: 512,
+ timeout: Duration.seconds(10),
+ environment: {
+ LOG_LEVEL: buildConfig.Parameters.LogLevel,
+ DYNAMODB_TABLE_CANFRAMES: buildConfig.Parameters.CanframesTableName,
+ }
+ });
+
+ this.updateCanframeFunction = new Function(this, 'updateCanframe', {
+ functionName: `${buildConfig.Solution}-${buildConfig.App}-updateCanframe-${buildConfig.Environment}`,
+ runtime: Runtime.NODEJS_16_X,
+ code: updateCanframeCode,
+ handler: 'updateCanframeHandler.handler',
+ memorySize: 512,
+ timeout: Duration.seconds(10),
+ environment: {
+ LOG_LEVEL: buildConfig.Parameters.LogLevel,
+ DYNAMODB_TABLE_CANFRAMES: buildConfig.Parameters.CanframesTableName,
+ }
+ });
+
+ this.deleteCanframeFunction = new Function(this, 'deleteCanframe', {
+ functionName: `${buildConfig.Solution}-${buildConfig.App}-deleteCanframe-${buildConfig.Environment}`,
+ runtime: Runtime.NODEJS_16_X,
+ code: deleteCanframeCode,
+ handler: 'deleteCanframeHandler.handler',
+ memorySize: 512,
+ timeout: Duration.seconds(10),
+ environment: {
+ LOG_LEVEL: buildConfig.Parameters.LogLevel,
+ DYNAMODB_TABLE_CANFRAMES: buildConfig.Parameters.CanframesTableName,
+ }
+ });
+
+ // defines Rest API gateway with one CRUD methods
+ this.restAPI = new RestApi(this, 'CanframeAPI', {
+ restApiName: `${buildConfig.Solution}-${buildConfig.App}-CanframeAPI-${buildConfig.Environment}`,
+ });
+
+ this.canframesResource = this.restAPI.root.addResource('canframes');
+ this.canframesResource.addMethod('GET', new LambdaIntegration(this.getCanframesFunction, {}));
+ this.canframesResource.addMethod('POST', new LambdaIntegration(this.createCanframeFunction, {}));
+ this.canframesResource.addMethod('PUT', new LambdaIntegration(this.updateCanframeFunction, {}));
+ this.canframesResource.addMethod('DELETE', new LambdaIntegration(this.deleteCanframeFunction, {}));
+ this.canframesResource.addResource('{vinId}').addMethod('GET', new LambdaIntegration(this.getCanframeFunction, {}));
+
+ // grant the lambda role read/write permissions to our table
+ canframesTable.grantReadWriteData(this.getCanframesFunction);
+ canframesTable.grantReadWriteData(this.getCanframeFunction);
+ canframesTable.grantReadWriteData(this.createCanframeFunction);
+ canframesTable.grantReadWriteData(this.updateCanframeFunction);
+ canframesTable.grantReadWriteData(this.deleteCanframeFunction);
+ }
+}
+
+export default CanframesRestApiStack;
diff --git a/source/packages/services/sample-service/jest.config.ts b/source/packages/services/sample-service/jest.config.ts
new file mode 100644
index 0000000..d13fe21
--- /dev/null
+++ b/source/packages/services/sample-service/jest.config.ts
@@ -0,0 +1,26 @@
+import type { JestConfigWithTsJest } from 'ts-jest'
+
+const config: JestConfigWithTsJest = {
+ preset: 'ts-jest/presets/default-esm',
+ testEnvironment: 'node',
+ roots: ['/test/jest'],
+ setupFiles: ['/test/jest/setup.ts'],
+ testMatch: ['**/*.spec.ts', '**/*.test.ts'],
+ moduleNameMapper: {
+ '^(\\.{1,2}/.*)\\.js$': '$1',
+ "uuid": require.resolve('uuid'),
+ },
+ transform: {
+ '^.+\\.tsx?$': ['ts-jest', { useESM: true }]
+ },
+ transformIgnorePatterns: ["/node_modules/.+.(js|jsx)$"],
+ verbose: true,
+ globals: {
+ jest: {
+ testEnvironment: 'node',
+ setupFiles: ['/test/jest/setup.ts'],
+ }
+ },
+}
+
+export default config;
diff --git a/source/packages/services/sample-service/package.json b/source/packages/services/sample-service/package.json
new file mode 100644
index 0000000..5c6a78c
--- /dev/null
+++ b/source/packages/services/sample-service/package.json
@@ -0,0 +1,78 @@
+{
+ "name": "@hanhdt/sample-service",
+ "version": "0.1.1",
+ "publishConfig": {
+ "directory": "dist"
+ },
+ "bin": {
+ "canframes": "infrastructure/canframesApp.ts"
+ },
+ "scripts": {
+ "clean": "npx shx rm -rf dist tsconfig.tsbuildinfo .rush .nyc_output *.log",
+ "build": "tsc",
+ "watch": "tsc -w",
+ "cdk": "cdk",
+ "deploy:dev": "cdk --app dist/infrastructure/canframesApp.js deploy -c config=dev",
+ "deploy:prod": "cdk --app dist/infrastructure/canframesApp.js deploy -c config=prod",
+ "destroy:dev": "cdk --app dist/infrastructure/canframesApp.js destroy -c config=dev",
+ "destroy:prod": "cdk --app dist/infrastructure/canframesApp.js destroy -c config=prod",
+ "diff:dev": "cdk --app 'npx ts-node infrastructure/canframesApp.ts' diff -c config=dev",
+ "diff:prod": "cdk --app 'npx ts-node infrastructure/canframesApp.ts' diff -c config=prod",
+ "test:jest": "jest --coverage --detectOpenHandles",
+ "test:cucumber": "./node_modules/.bin/cucumber-js -p default",
+ "coverage:cucumber": "nyc npm run test:cucumber --reporter=lcov --reporter=text-summary"
+ },
+ "devDependencies": {
+ "@cucumber/cucumber": "7.3.2",
+ "@cucumber/pretty-formatter": "^1.0.0",
+ "@istanbuljs/nyc-config-typescript": "^1.0.2",
+ "@mrgrain/cdk-esbuild": "^4.1.5",
+ "@rushstack/eslint-config": "2.3.4",
+ "@types/aws-lambda": "^8.10.114",
+ "@types/chai": "^4.3.4",
+ "@types/chai-string": "^1.4.2",
+ "@types/chai-uuid": "^1.0.2",
+ "@types/jest": "^29.5.1",
+ "@types/node": "^18.14.6",
+ "@types/superagent": "4.1.15",
+ "@types/uuid": "^9.0.1",
+ "aws-cdk": "2.73.0",
+ "aws-sdk": "^2.1353.0",
+ "chai": "^4.3.7",
+ "chai-string": "^1.5.0",
+ "chai-uuid": "^1.0.6",
+ "jest": "^29.5.0",
+ "nyc": "^15.1.0",
+ "superagent": "7.1.1",
+ "ts-jest": "^29.0.5",
+ "ts-node": "^10.9.1",
+ "typescript": "~4.9.5"
+ },
+ "dependencies": {
+ "@hanhdt/logger": "^1.0.0",
+ "@hanhdt/config-inject": "^1.0.0",
+ "aws-cdk-lib": "2.73.0",
+ "constructs": "^10.0.0",
+ "ow": "0.23.0",
+ "uuid": "^9.0.0"
+ },
+ "nyc": {
+ "extends": "@istanbuljs/nyc-config-typescript",
+ "include": [
+ "src/**/*.ts"
+ ],
+ "exclude": [
+ "src/**/*.spec.ts",
+ "src/**/*.test.ts",
+ "src/**/*.steps.ts",
+ "src/**/*.feature.ts"
+ ],
+ "reporter": [
+ "text",
+ "html"
+ ],
+ "all": true,
+ "check-coverage": true,
+ "lines": 100
+ }
+}
diff --git a/source/packages/services/sample-service/src/canframe/canframe.dao.ts b/source/packages/services/sample-service/src/canframe/canframe.dao.ts
new file mode 100644
index 0000000..378d40b
--- /dev/null
+++ b/source/packages/services/sample-service/src/canframe/canframe.dao.ts
@@ -0,0 +1,94 @@
+import { DynamoDB } from "aws-sdk";
+import { CanframeList, CanframeItem } from './canframe.model';
+import { logger } from '../utils/logger';
+
+
+// DynamoDB client instance
+const ddb = new DynamoDB.DocumentClient();
+
+const _scanCanframes = async (tableName: string) : Promise => {
+ return await ddb.scan({ TableName: tableName }).promise();
+};
+
+export const listCanframes = async (tableName: string, year: string, sort: string) : Promise => {
+ try {
+ const ddbItems = await _scanCanframes(tableName);
+ const yearKey = year;
+ const sortKeys = sort;
+
+ let result : CanframeList = { canframes: [] };
+ logger.debug(`canframes.dao::listCanframesDummy: exit`, ddbItems.Items, yearKey, sortKeys);
+
+ if (ddbItems?.Items?.length) {
+ for (const item of ddbItems.Items) {
+ let cItem = new CanframeItem();
+ cItem.id = item.id;
+ cItem.year = item.year;
+ cItem.model = item.model;
+ result.canframes.push(cItem);
+ }
+ }
+
+ return result;
+ } catch (e) {
+ throw e;
+ }
+};
+
+export const singleCanframe = async (tableName: string, vinId: string, year: string) : Promise => {
+ try {
+ const ddbItem = await ddb.get({
+ TableName: tableName,
+ Key: {
+ id: vinId,
+ year: year
+ }
+ }).promise();
+ const canframe = ddbItem.Item as CanframeItem;
+ logger.debug(`canframes.dao::singleCanframe: exit: ${JSON.stringify(canframe)}`);
+ return canframe;
+ } catch (e) {
+ logger.error(`canframes.dao::singleCanframe: error: ${e}`);
+ throw e;
+ }
+};
+
+export const createCanframe = async (tableName: string, canframeItem: CanframeItem) => {
+ try {
+ // save the canframe to the dynamodb
+ await ddb.put({
+ TableName: tableName,
+ Item: canframeItem as DynamoDB.DocumentClient.PutItemInputAttributeMap
+ }).promise();
+ logger.debug(`canframes.dao::createCanframe: exit: ${JSON.stringify(canframeItem)}`);
+
+ return canframeItem;
+ } catch (e) {
+ logger.error(`canframes.controller::createCanframe: error: ${e}`);
+ throw e;
+ }
+};
+
+export const updateCanframe = async (tableName: string, canframeItem: CanframeItem) => {
+ try {
+ // save the canframe to the dynamodb
+ await ddb.put({
+ TableName: tableName,
+ Item: canframeItem as DynamoDB.DocumentClient.PutItemInputAttributeMap
+ }).promise();
+ logger.debug(`canframes.dao::updateCanframe: exit: ${JSON.stringify(canframeItem)}`);
+ return canframeItem;
+ } catch (e) {
+ logger.error(`canframes.dao::updateCanframe: error: ${e}`);
+ throw e;
+ }
+};
+
+export const deleteCanframe = async (tableName: string, vinId: string) => {
+ try {
+ logger.debug(`canframes.dao::deleteCanframe: exit: ${tableName} - ${vinId}`);
+ } catch (e) {
+ logger.error(`canframes.dao::deleteCanframe: error: ${e}`);
+ throw e;
+ }
+};
\ No newline at end of file
diff --git a/source/packages/services/sample-service/src/canframe/canframe.model.ts b/source/packages/services/sample-service/src/canframe/canframe.model.ts
new file mode 100644
index 0000000..d268458
--- /dev/null
+++ b/source/packages/services/sample-service/src/canframe/canframe.model.ts
@@ -0,0 +1,25 @@
+export class CanframeItem {
+ id!: string;
+ year!: string;
+ model!: string;
+}
+
+export interface CanframeList {
+ canframes:CanframeItem[];
+ pagination?: {
+ token:string,
+ limit:number
+ };
+}
+
+export interface CanframeListArgs {
+ year:string;
+ sort?:SortDirection;
+ token?:string;
+ limit?:number;
+}
+
+export enum SortDirection {
+ asc = 'asc',
+ desc = 'desc'
+}
\ No newline at end of file
diff --git a/source/packages/services/sample-service/src/canframe/canframe.service.ts b/source/packages/services/sample-service/src/canframe/canframe.service.ts
new file mode 100644
index 0000000..c6d611e
--- /dev/null
+++ b/source/packages/services/sample-service/src/canframe/canframe.service.ts
@@ -0,0 +1,60 @@
+/** Canframe Service Class */
+import ow from 'ow';
+import * as uuid from 'uuid';
+
+import { listCanframes, singleCanframe, createCanframe, updateCanframe, deleteCanframe } from './canframe.dao';
+import { CanframeList, CanframeItem } from './canframe.model';
+import { logger } from '../utils/logger';
+
+export async function listCanframesDummy(tableName: string, year: string, sort: string) : Promise {
+ logger.debug(`canframes.service::listCanframesDummy: in: year:${year}, sort: ${sort}, table: ${tableName}`);
+
+ ow(year,'year', ow.string.nonEmpty);
+
+ const canframes:CanframeItem[] = [];
+ const rst:CanframeList = {
+ canframes
+ };
+
+ const canframeDDBItems = await listCanframes(tableName, year, sort);
+
+ if (canframeDDBItems?.canframes.length > 0) {
+ rst.canframes = canframeDDBItems.canframes;
+ }
+
+ logger.debug(`canframes.service::listCanframesDummy: exit`, rst);
+ return rst;
+}
+
+export async function singleCanframeDummy(tableName: string, vinId: string, year: string) : Promise {
+ const canframe = await singleCanframe(tableName, vinId, year);
+ logger.debug(`canframes.service::singleCanframeDummy:`, canframe);
+ return canframe;
+}
+
+export async function createCanframeDummy(tableName: string, canframeParams: any) : Promise {
+ const canframe = new CanframeItem();
+ canframe.id = uuid.v4();
+ canframe.year = canframeParams.year;
+ canframe.model = canframeParams.model;
+ await createCanframe(tableName, canframe);
+ logger.debug(`canframes.service::createCanframeDummy exit: canframe: ${JSON.stringify(canframe)}`);
+ return canframe;
+}
+
+export async function updateCanframeDummy(tableName: string, vinId: string, canframeParams: any) : Promise {
+ const updatedCanframe = new CanframeItem();
+ updatedCanframe.id = canframeParams.id;
+ updatedCanframe.year = canframeParams.year;
+ updatedCanframe.model = canframeParams.model;
+ await updateCanframe(tableName, updatedCanframe);
+
+ logger.debug(`canframes.service::updateCanframeDummy exit: canframe ${vinId}: ${JSON.stringify(updatedCanframe)}`);
+ return updatedCanframe;
+}
+
+export async function deleteCanframeDummy(tableName: string, vinId: string) {
+ await deleteCanframe(tableName, vinId);
+ logger.debug(`canframes.service::deleteCanframeDummy exit`);
+ return;
+}
diff --git a/source/packages/services/sample-service/src/createCanframeHandler.ts b/source/packages/services/sample-service/src/createCanframeHandler.ts
new file mode 100644
index 0000000..7fd489f
--- /dev/null
+++ b/source/packages/services/sample-service/src/createCanframeHandler.ts
@@ -0,0 +1,38 @@
+import ow from 'ow';
+import { APIGatewayProxyHandler, APIGatewayProxyResult, APIGatewayProxyEvent, Context } from 'aws-lambda';
+
+import { createCanframeDummy } from './canframe/canframe.service';
+import { buildLambdaHttpResponse } from './utils/helpers';
+import { logger } from './utils/logger';
+
+const tableName = process.env.DYNAMODB_TABLE_CANFRAMES;
+
+export const handler: APIGatewayProxyHandler = async (
+ lambdaEvent: APIGatewayProxyEvent,
+ lambdaContext: Context
+): Promise => {
+ logger.debug("context:", lambdaContext);
+ logger.debug("request:", lambdaEvent);
+
+ ow(tableName, ow.string.nonEmpty);
+
+ const canframe = await createCanframeStep(tableName, lambdaEvent.body);
+
+ const response: APIGatewayProxyResult = buildLambdaHttpResponse({
+ statusCode: 200,
+ status: true,
+ statusMessage: 'Success',
+ data: { canframe },
+ })
+
+ return response;
+}
+
+const createCanframeStep = async (tableName: string, _bodyParams: any) => {
+ try {
+ const canframe = await createCanframeDummy(tableName, _bodyParams);
+ return canframe;
+ } catch (e) {
+ logger.error(`canframes.controller::createCanframeStep: error: ${e}`);
+ }
+};
\ No newline at end of file
diff --git a/source/packages/services/sample-service/src/deleteCanframeHandler.ts b/source/packages/services/sample-service/src/deleteCanframeHandler.ts
new file mode 100644
index 0000000..d1e1158
--- /dev/null
+++ b/source/packages/services/sample-service/src/deleteCanframeHandler.ts
@@ -0,0 +1,33 @@
+import ow from 'ow';
+import { APIGatewayProxyHandler, APIGatewayProxyResult, APIGatewayProxyEvent, Context } from 'aws-lambda';
+
+import { deleteCanframeDummy } from './canframe/canframe.service';
+import { buildLambdaHttpResponse } from './utils/helpers';
+import { logger } from './utils/logger';
+
+
+const tableName = process.env.DYNAMODB_TABLE_CANFRAMES;
+
+export const handler: APIGatewayProxyHandler = async (
+ lambdaEvent: APIGatewayProxyEvent,
+ lambdaContext: Context
+): Promise => {
+ logger.debug("context:", lambdaContext);
+ logger.debug("request:", lambdaEvent);
+
+ const vinId = lambdaEvent.pathParameters?.vinId;
+
+ ow(tableName, ow.string.nonEmpty);
+ ow(vinId, ow.string.nonEmpty);
+
+ await deleteCanframeDummy(tableName, vinId);
+
+ const response: APIGatewayProxyResult = buildLambdaHttpResponse({
+ statusCode: 200,
+ status: true,
+ statusMessage: 'Success',
+ data: { message: 'Delete a canframe!' },
+ })
+
+ return response;
+}
\ No newline at end of file
diff --git a/source/packages/services/sample-service/src/getCanframeHandler.ts b/source/packages/services/sample-service/src/getCanframeHandler.ts
new file mode 100644
index 0000000..eba058b
--- /dev/null
+++ b/source/packages/services/sample-service/src/getCanframeHandler.ts
@@ -0,0 +1,33 @@
+import ow from 'ow';
+import { APIGatewayProxyHandler, APIGatewayProxyResult, APIGatewayProxyEvent, Context } from 'aws-lambda';
+
+import { buildLambdaHttpResponse } from './utils/helpers';
+import { singleCanframeDummy } from './canframe/canframe.service';
+import { logger } from './utils/logger';
+
+const tableName = process.env.DYNAMODB_TABLE_CANFRAMES;
+
+export const handler: APIGatewayProxyHandler = async (
+ lambdaEvent: APIGatewayProxyEvent,
+ lambdaContext: Context
+): Promise => {
+ logger.debug("context:", lambdaContext);
+ logger.debug("request:", lambdaEvent);
+
+ const vinId = lambdaEvent.pathParameters?.vinId;
+ const year = lambdaEvent.queryStringParameters?.year;
+
+ ow(tableName, ow.string.nonEmpty);
+ ow(vinId, ow.string.nonEmpty);
+
+ const canframe = await singleCanframeDummy(tableName, vinId, year ?? '2023');
+
+ const response: APIGatewayProxyResult = buildLambdaHttpResponse({
+ statusCode: 200,
+ status: true,
+ statusMessage: 'Success',
+ data: { canframe },
+ });
+
+ return response;
+}
\ No newline at end of file
diff --git a/source/packages/services/sample-service/src/getCanframesHandler.ts b/source/packages/services/sample-service/src/getCanframesHandler.ts
new file mode 100644
index 0000000..9c4dc23
--- /dev/null
+++ b/source/packages/services/sample-service/src/getCanframesHandler.ts
@@ -0,0 +1,39 @@
+import ow from 'ow';
+import { APIGatewayProxyHandler, APIGatewayProxyResult, APIGatewayProxyEvent, Context } from 'aws-lambda';
+
+import { listCanframesDummy } from './canframe/canframe.service';
+import { CanframeList } from './canframe/canframe.model';
+import { buildLambdaHttpResponse } from './utils/helpers';
+import { logger } from './utils/logger';
+
+const tableName = process.env.DYNAMODB_TABLE_CANFRAMES;
+
+export const handler: APIGatewayProxyHandler = async (
+ lambdaEvent: APIGatewayProxyEvent,
+ lambdaContext: Context
+): Promise => {
+ logger.debug("context:", lambdaContext);
+ logger.debug("request:", lambdaEvent);
+
+ ow(tableName, ow.string.nonEmpty);
+
+ const listCanframes = await listCanframesStep(tableName, '2023', 'asc');
+
+ const response: APIGatewayProxyResult = buildLambdaHttpResponse({
+ statusCode: 200,
+ status: true,
+ statusMessage: 'Success',
+ data: { canframes: listCanframes.canframes },
+ });
+
+ return response;
+}
+
+const listCanframesStep = async (tableName: string, year: string, sort: string): Promise => {
+ try {
+ const canframes = await listCanframesDummy(tableName, year, sort);
+ return canframes;
+ } catch (e) {
+ throw e;
+ }
+};
\ No newline at end of file
diff --git a/source/packages/services/sample-service/src/updateCanframeHandler.ts b/source/packages/services/sample-service/src/updateCanframeHandler.ts
new file mode 100644
index 0000000..1a8d527
--- /dev/null
+++ b/source/packages/services/sample-service/src/updateCanframeHandler.ts
@@ -0,0 +1,42 @@
+import ow from 'ow';
+import { APIGatewayProxyHandler, APIGatewayProxyResult, APIGatewayProxyEvent, Context } from 'aws-lambda';
+
+import { updateCanframeDummy } from './canframe/canframe.service';
+import { buildLambdaHttpResponse } from './utils/helpers';
+import { logger } from './utils/logger';
+
+
+const tableName = process.env.DYNAMODB_TABLE_CANFRAMES;
+
+export const handler: APIGatewayProxyHandler = async (
+ lambdaEvent: APIGatewayProxyEvent,
+ lambdaContext: Context
+): Promise => {
+ logger.debug("context:", lambdaContext);
+ logger.debug("request:", lambdaEvent);
+
+ const vinId = lambdaEvent.pathParameters?.vinId;
+
+ ow(tableName, ow.string.nonEmpty);
+ ow(vinId, ow.string.nonEmpty);
+
+ const updatedCanframe = await updateCanframeStep(tableName, vinId, lambdaEvent.body);
+
+ const response: APIGatewayProxyResult = buildLambdaHttpResponse({
+ statusCode: 200,
+ status: true,
+ statusMessage: 'Success',
+ data: { canframe: updatedCanframe },
+ })
+
+ return response;
+}
+
+const updateCanframeStep = async (tableName: string, vinId: string, _bodyParams: any) => {
+ try {
+ const canframe = await updateCanframeDummy(tableName, vinId, _bodyParams);
+ return canframe;
+ } catch (e) {
+ logger.error(`canframes.controller::createCanframeStep: error: ${e}`);
+ }
+};
\ No newline at end of file
diff --git a/source/packages/services/sample-service/src/utils/errors.ts b/source/packages/services/sample-service/src/utils/errors.ts
new file mode 100644
index 0000000..aee5646
--- /dev/null
+++ b/source/packages/services/sample-service/src/utils/errors.ts
@@ -0,0 +1,11 @@
+export interface ErrorWithResponse extends Error {
+ response?: {
+ status: number;
+ text: string;
+ body: any;
+ };
+}
+
+export function handleError(e: ErrorWithResponse): string {
+ return `handleError: ${JSON.stringify(e)}`;
+}
diff --git a/source/packages/services/sample-service/src/utils/helpers.ts b/source/packages/services/sample-service/src/utils/helpers.ts
new file mode 100644
index 0000000..159538b
--- /dev/null
+++ b/source/packages/services/sample-service/src/utils/helpers.ts
@@ -0,0 +1,87 @@
+import { APIGatewayProxyResult } from "aws-lambda";
+
+export interface ResponseParams {
+ headers?: {};
+ statusCode: number;
+ status: boolean;
+ statusMessage: string;
+ data?: any;
+}
+
+export interface Response {
+ statusCode: number;
+ headers: any;
+ body: string;
+}
+
+const _buildResponseHeaders = (originHeaders: any) => {
+ let headers = { ...originHeaders };
+
+ /** CORS Headers response */
+ headers['Access-Control-Allow-Origin'] = '*';
+ headers['Access-Control-Allow-Credentials'] = true;
+
+ return headers;
+};
+
+
+/**
+ * Attempts to parse a JSON string to JS object, return null if fails.
+ * @param {string} str
+ * @returns {object|null}
+ */
+export function tryParseJSON(str: string) {
+ try {
+ if (str === '') {
+ return null;
+ } else {
+ return JSON.parse(str.replace(/\n/g, ''));
+ }
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * Builds a standard HTTP response object for Lambda function handlers.
+ * @example
+ * buildLambdaHttpResponse()
+ *
+ * @example
+ * buildLambdaHttpResponse({
+ * data: { key: 'value' },
+ * })
+ *
+ * @example
+ * buildLambdaHttpResponse({
+ * status: false,
+ * statusMessage: 'Internal Server Error',
+ * statusCode: 500,
+ * })
+ */
+export function buildLambdaHttpResponse(params: ResponseParams = {
+ headers: {},
+ statusCode: 200,
+ status: true,
+ statusMessage: 'Success',
+ data: null
+}): APIGatewayProxyResult {
+ let { headers, statusCode, statusMessage, data, status } = params;
+
+ const responseBody = {
+ status: status,
+ statusMessage: statusMessage,
+ data: data,
+ };
+
+ let response = {} as APIGatewayProxyResult;
+
+ headers = _buildResponseHeaders(headers);
+ response = {
+ statusCode: statusCode,
+ headers: headers,
+ body: JSON.stringify(responseBody, null, 2),
+ };
+
+ return response;
+}
diff --git a/source/packages/services/sample-service/src/utils/logger.ts b/source/packages/services/sample-service/src/utils/logger.ts
new file mode 100644
index 0000000..7e7f147
--- /dev/null
+++ b/source/packages/services/sample-service/src/utils/logger.ts
@@ -0,0 +1,6 @@
+import { CDFLogger } from '@hanhdt/logger';
+
+const LOG_LEVEL = process.env.LOG_LEVEL || 'info';
+
+// Common logger library instance
+export const logger = new CDFLogger(LOG_LEVEL);
\ No newline at end of file
diff --git a/source/packages/services/sample-service/test/cucumber/features/getCanframe.feature b/source/packages/services/sample-service/test/cucumber/features/getCanframe.feature
new file mode 100644
index 0000000..2cf5e60
--- /dev/null
+++ b/source/packages/services/sample-service/test/cucumber/features/getCanframe.feature
@@ -0,0 +1,7 @@
+Feature: Canframe API
+
+ Scenario: Can retrive a canframe by id
+ Given I have a canframeId "1"
+ When I retrieve the canframe by id
+ Then the canframe response status should be "200"
+ And the canframe response should contain "canframe"
\ No newline at end of file
diff --git a/source/packages/services/sample-service/test/cucumber/features/getCanframes.feature b/source/packages/services/sample-service/test/cucumber/features/getCanframes.feature
new file mode 100644
index 0000000..6d7d12a
--- /dev/null
+++ b/source/packages/services/sample-service/test/cucumber/features/getCanframes.feature
@@ -0,0 +1,7 @@
+Feature: Canframes API
+
+ Scenario: Can retrieve a list of canframes with all attributes
+ When I retrieve all canframes
+ Then the canframes response status should be "200"
+ And the canframes response should be in JSON format
+ And the canframes response should contain "canframes"
\ No newline at end of file
diff --git a/source/packages/services/sample-service/test/cucumber/features/tryParseJSON.feature b/source/packages/services/sample-service/test/cucumber/features/tryParseJSON.feature
new file mode 100644
index 0000000..72bbaf1
--- /dev/null
+++ b/source/packages/services/sample-service/test/cucumber/features/tryParseJSON.feature
@@ -0,0 +1,19 @@
+Feature: tryParseJSON function
+
+ #1
+ Scenario: check tryParseJSON with Object string
+ Given I have a valid JSON string
+ When I attempt to parse the JSON string with tryParseJSON
+ Then I should get an JS object back
+
+ #2
+ Scenario: check tryParseJSON with Array string
+ Given I have an array string
+ When I attempt to parse the array string with tryParseJSON
+ Then I should get an JS array back
+
+ #Scenario 3
+ Scenario: check tryParseJSON with empty string
+ Given I have an empty string
+ When I attempt to parse the empty string with tryParseJSON
+ Then I should get null back
\ No newline at end of file
diff --git a/source/packages/services/sample-service/test/cucumber/step-definitions/getCanframe.steps.ts b/source/packages/services/sample-service/test/cucumber/step-definitions/getCanframe.steps.ts
new file mode 100644
index 0000000..908b6e9
--- /dev/null
+++ b/source/packages/services/sample-service/test/cucumber/step-definitions/getCanframe.steps.ts
@@ -0,0 +1,62 @@
+import { Given, When, Then } from "@cucumber/cucumber";
+import * as request from 'superagent';
+import { fail } from "assert";
+import { assert } from "chai";
+import { APIGatewayProxyEvent, Context } from 'aws-lambda';
+
+import { logger } from '../../../src/utils/logger';
+import { handler as getCanframeHandler } from "../../../src/getCanframeHandler";
+
+const RESULTS = 'results';
+const RESPONSE_STATUS = 'responseStatus';
+
+// Scenario 2
+Given('I have a canframeId {string}', function (id: string) {
+ this.canframeId = id;
+});
+
+When('I retrieve the canframe by id', async function () {
+ if (process.env.TEST_MODE === "local"){
+ // logger.info("local lambda test mode");
+ const lambdaEvent: APIGatewayProxyEvent = {
+ body: null,
+ headers: {},
+ multiValueHeaders: {},
+ httpMethod: "GET",
+ isBase64Encoded: false,
+ path: '',
+ pathParameters: {},
+ queryStringParameters: {},
+ multiValueQueryStringParameters: {},
+ stageVariables: {},
+ requestContext: {} as any,
+ resource: "",
+ };
+ const lambdaContext: Context = null as any;
+
+ try {
+ const resp = await getCanframeHandler(lambdaEvent, lambdaContext, null as any);
+ logger.info(resp as any);
+ } catch (err) {
+ fail(`Expected response, instead: ${err}`);
+ }
+ } else {
+ try {
+ // logger.info("api test mode");
+ const url = `https://vc90vyprf1.execute-api.ap-northeast-1.amazonaws.com/prod/canframes/${this.canframeId}`;
+ const resp = await request.get(url);
+ this[RESULTS] = resp.body;
+ this[RESPONSE_STATUS] = resp.status.toString();
+ } catch (err) {
+ fail(`Expected response, instead: ${err}`);
+ }
+ }
+});
+
+Then('the canframe response status should be {string}', function (status: string) {
+ assert.equal(this[RESPONSE_STATUS], status);
+});
+
+Then('the canframe response should contain "canframe"', function () {
+ assert.containsAllKeys(this[RESULTS].data, ["canframe"]);
+});
\ No newline at end of file
diff --git a/source/packages/services/sample-service/test/cucumber/step-definitions/getCanframes.steps.ts b/source/packages/services/sample-service/test/cucumber/step-definitions/getCanframes.steps.ts
new file mode 100644
index 0000000..9154e50
--- /dev/null
+++ b/source/packages/services/sample-service/test/cucumber/step-definitions/getCanframes.steps.ts
@@ -0,0 +1,67 @@
+import { When, Then } from "@cucumber/cucumber";
+import * as request from 'superagent';
+import { fail } from "assert";
+import { assert } from "chai";
+import { APIGatewayProxyEvent, Context } from 'aws-lambda';
+
+import { logger } from '../../../src/utils/logger';
+import { handler as getCanframesHandler } from "../../../src/getCanframesHandler";
+
+
+const RESULTS = 'results';
+const RESPONSE_STATUS = 'responseStatus';
+
+// Scenario 1
+When('I retrieve all canframes', async function () {
+ // Write code here that turns the phrase above into concrete actions
+ if (process.env.TEST_MODE === "local"){
+ // logger.info("local lambda test mode");
+ const lambdaEvent: APIGatewayProxyEvent = {
+ body: null,
+ headers: {},
+ multiValueHeaders: {},
+ httpMethod: "GET",
+ isBase64Encoded: false,
+ path: '',
+ pathParameters: {},
+ queryStringParameters: {},
+ multiValueQueryStringParameters: {},
+ stageVariables: {},
+ requestContext: {} as any,
+ resource: "",
+ };
+ const lambdaContext: Context = null as any;
+
+ try {
+ const resp = await getCanframesHandler(lambdaEvent, lambdaContext, null as any);
+ logger.info(resp as any);
+ // const result = JSON.parse(resp["body"] as any);
+ // this[RESULTS] = resp["body"];
+ } catch (err) {
+ fail(`Expected response, instead: ${err}`);
+ }
+ } else {
+ try {
+ // logger.info("api test mode");
+ const url = `https://vc90vyprf1.execute-api.ap-northeast-1.amazonaws.com/prod/canframes`;
+ const resp = await request.get(url);
+ // logger.info(resp.body);
+ this[RESULTS] = resp.body;
+ this[RESPONSE_STATUS] = resp.status.toString();
+ } catch (err) {
+ fail(`Expected response, instead: ${err}`);
+ }
+ }
+});
+
+Then('the canframes response status should be {string}', function (status: string) {
+ assert.equal(this[RESPONSE_STATUS], status);
+});
+
+Then('the canframes response should be in JSON format', function () {
+ assert.isObject(this[RESULTS]);
+});
+
+Then('the canframes response should contain "canframes"', function () {
+ assert.containsAllKeys(this[RESULTS].data, ["canframes"]);
+});
\ No newline at end of file
diff --git a/source/packages/services/sample-service/test/cucumber/step-definitions/tryParseJSON.steps.ts b/source/packages/services/sample-service/test/cucumber/step-definitions/tryParseJSON.steps.ts
new file mode 100644
index 0000000..9b6e914
--- /dev/null
+++ b/source/packages/services/sample-service/test/cucumber/step-definitions/tryParseJSON.steps.ts
@@ -0,0 +1,56 @@
+import { Given, When, Then } from "@cucumber/cucumber";
+import { fail } from "assert";
+import { assert } from "chai";
+
+import { tryParseJSON } from "../../../src/utils/helpers";
+
+// Scenario: I have a valid JSON string
+Given('I have a valid JSON string', function () {
+ this.jsonString = '{"key": "value"}';
+});
+
+When('I attempt to parse the JSON string with tryParseJSON', function () {
+ try {
+ this.jsonStringResult = tryParseJSON(this.jsonString);
+ } catch (err) {
+ fail(`Expected response, instead: ${err}`);
+ }
+});
+
+Then('I should get an JS object back', function () {
+ assert.isObject(this.jsonStringResult);
+});
+
+// Scenario: I have an array string
+Given('I have an array string', function () {
+ this.jsonArrayString = '["key", "value"]';
+});
+
+When('I attempt to parse the array string with tryParseJSON', function () {
+ try {
+ this.jsonArrayStringResult = tryParseJSON(this.jsonArrayString);
+ } catch (err) {
+ fail(`Expected response, instead: ${err}`);
+ }
+});
+
+Then('I should get an JS array back', function () {
+ assert.isArray(this.jsonArrayStringResult);
+});
+
+// Scenario: I have an invalid JSON string
+Given('I have an empty string', function () {
+ this.emptyString = '';
+});
+
+When('I attempt to parse the empty string with tryParseJSON', function () {
+ try {
+ this.emptyStringResult = tryParseJSON(this.emptyString);
+ } catch (err) {
+ fail(`Expected response, instead: ${err}`);
+ }
+});
+
+Then('I should get null back', function () {
+ assert.isNull(this.emptyStringResult);
+});
\ No newline at end of file
diff --git a/source/packages/services/sample-service/test/cucumber/support/canframeHooks.ts b/source/packages/services/sample-service/test/cucumber/support/canframeHooks.ts
new file mode 100644
index 0000000..e69de29
diff --git a/source/packages/services/sample-service/test/cucumber/utils/qs.helper.ts b/source/packages/services/sample-service/test/cucumber/utils/qs.helper.ts
new file mode 100644
index 0000000..24866b9
--- /dev/null
+++ b/source/packages/services/sample-service/test/cucumber/utils/qs.helper.ts
@@ -0,0 +1,25 @@
+export class QSHelper {
+ public static getQueryParams(queryString: string): any[] {
+ if (queryString === '___null___' || queryString === '___undefined___') {
+ return [];
+ } else {
+ let queryParams = queryString.split('?')[1];
+ if (queryParams === undefined) {
+ queryParams = queryString.split('?')[0]
+ }
+ const params = queryParams.split('&');
+
+ let pair: string[] = [];
+ const data: any[] = [];
+
+ params.forEach(function (d) {
+ pair = d.split('=') as string[];
+ data.push({
+ key: pair[0],
+ value: pair[1]
+ });
+ });
+ return data;
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/packages/services/sample-service/test/jest/canframe/canframe.dao.spec.ts b/source/packages/services/sample-service/test/jest/canframe/canframe.dao.spec.ts
new file mode 100644
index 0000000..b752059
--- /dev/null
+++ b/source/packages/services/sample-service/test/jest/canframe/canframe.dao.spec.ts
@@ -0,0 +1,49 @@
+import AWS from "aws-sdk";
+import { listCanframes } from "../../../src/canframe/canframe.dao";
+
+AWS.config.update({ region: 'ap-northeast-1' });
+
+describe("canframe.dao tests", () => {
+ beforeEach(() => {
+ const ddbData = {
+ Items: [{ id: "1", model: "tanto-01", year: "2023" }],
+ Count: 0,
+ ScannedCount: 0
+ };
+
+ const mockDDbScan = jest.fn().mockImplementation(() => {
+ return {
+ promise() {
+ return Promise.resolve(ddbData);
+ },
+ };
+ });
+
+ jest.mock("aws-sdk", () => {
+ return {
+ DynamoDB: {
+ DocumentClient: jest.fn(() => ({
+ scan: mockDDbScan,
+ })),
+ },
+ };
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("check listCanframes", async () => {
+ const expectedData = {
+ canframes: [
+ { id: "1", model: "tanto-01", year: "2023" },
+ ]
+ };
+ const tableName = "canframes";
+ const year = "2023";
+ const sort = "model";
+ const canframes = await listCanframes(tableName, year, sort);
+ expect(canframes).toEqual(expectedData);
+ });
+});
\ No newline at end of file
diff --git a/source/packages/services/sample-service/test/jest/canframe/canframe.model.spec.ts b/source/packages/services/sample-service/test/jest/canframe/canframe.model.spec.ts
new file mode 100644
index 0000000..ea2d6bc
--- /dev/null
+++ b/source/packages/services/sample-service/test/jest/canframe/canframe.model.spec.ts
@@ -0,0 +1,43 @@
+import { expect } from "chai";
+import { CanframeItem } from "../../../src/canframe/canframe.model";
+
+describe("canframe.model tests", () => {
+ test("check CanframeItem model", () => {
+ const canframeItem = new CanframeItem();
+ canframeItem.id = "id";
+ canframeItem.year = "year";
+ canframeItem.model = "model";
+
+ expect(canframeItem).to.deep.equal({
+ id: "id",
+ year: "year",
+ model: "model",
+ });
+ });
+
+ test("check CanframeItem model with empty values", () => {
+ const canframeItem = new CanframeItem();
+ canframeItem.id = "";
+ canframeItem.year = "";
+ canframeItem.model = "";
+
+ expect(canframeItem).to.deep.equal({
+ id: "",
+ year: "",
+ model: "",
+ });
+ });
+
+ test("check CanframeItem model with null values", () => {
+ const canframeItem = new CanframeItem();
+ canframeItem.id = null as any;
+ canframeItem.year = null as any;
+ canframeItem.model = null as any;
+
+ expect(canframeItem).to.deep.equal({
+ id: null,
+ year: null,
+ model: null,
+ });
+ });
+});
\ No newline at end of file
diff --git a/source/packages/services/sample-service/test/jest/canframe/canframe.service.spec.ts b/source/packages/services/sample-service/test/jest/canframe/canframe.service.spec.ts
new file mode 100644
index 0000000..32054e1
--- /dev/null
+++ b/source/packages/services/sample-service/test/jest/canframe/canframe.service.spec.ts
@@ -0,0 +1,46 @@
+import { listCanframesDummy } from '../../../src/canframe/canframe.service';
+
+describe("canframe.service tests", () => {
+ beforeEach(() => {
+ const ddbData = {
+ Items: [{ id: "1", model: "tanto-01", year: "2023" }],
+ Count: 0,
+ ScannedCount: 0
+ };
+
+ const mockDDbScan = jest.fn().mockImplementation(() => {
+ return {
+ promise() {
+ return Promise.resolve(ddbData);
+ },
+ };
+ });
+
+ jest.mock("aws-sdk", () => {
+ return {
+ DynamoDB: {
+ DocumentClient: jest.fn(() => ({
+ scan: mockDDbScan,
+ })),
+ },
+ };
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("check listCanframesDummy", async () => {
+ const expectedData = {
+ canframes: [
+ { id: "1", model: "tanto-01", year: "2023" },
+ ]
+ };
+ const tableName = "canframes";
+ const year = "2023";
+ const sort = "model";
+ const canframes = await listCanframesDummy(tableName, year, sort);
+ expect(canframes).toEqual(expectedData);
+ });
+});
\ No newline at end of file
diff --git a/source/packages/services/sample-service/test/jest/setup.ts b/source/packages/services/sample-service/test/jest/setup.ts
new file mode 100644
index 0000000..2def106
--- /dev/null
+++ b/source/packages/services/sample-service/test/jest/setup.ts
@@ -0,0 +1,2 @@
+import AWS from "aws-sdk";
+AWS.config.update({ region: 'ap-northeast-1' });
\ No newline at end of file
diff --git a/source/packages/services/sample-service/test/jest/utils/errors.spec.ts b/source/packages/services/sample-service/test/jest/utils/errors.spec.ts
new file mode 100644
index 0000000..f9d7fcb
--- /dev/null
+++ b/source/packages/services/sample-service/test/jest/utils/errors.spec.ts
@@ -0,0 +1,10 @@
+import { expect } from "chai";
+import { handleError, ErrorWithResponse } from "../../../src/utils/errors";
+
+describe("Error tests", () => {
+ test("check handleError", () => {
+ const error = new Error("test error") as ErrorWithResponse;
+ const result = handleError(error);
+ expect(result).to.equal(`handleError: ${JSON.stringify(error)}`);
+ });
+});
\ No newline at end of file
diff --git a/source/packages/services/sample-service/test/jest/utils/helpers.spec.ts b/source/packages/services/sample-service/test/jest/utils/helpers.spec.ts
new file mode 100644
index 0000000..74944b0
--- /dev/null
+++ b/source/packages/services/sample-service/test/jest/utils/helpers.spec.ts
@@ -0,0 +1,89 @@
+import { expect } from "chai";
+import { tryParseJSON, buildLambdaHttpResponse } from "../../../src/utils/helpers";
+
+describe("Helper tests", () => {
+ test("check tryParseJSON with Object string", () => {
+ const obj = { key: "value" };
+ const str = JSON.stringify(obj);
+ const result = tryParseJSON(str);
+
+ expect(result).to.deep.equal(obj);
+ });
+
+ test("check tryParseJSON with Array string", () => {
+ const arr = ["value1", "value2"];
+ const str = JSON.stringify(arr);
+ const result = tryParseJSON(str);
+
+ expect(result).to.deep.equal(arr);
+ });
+
+ test("check tryParseJSON with empty string", () => {
+ const str = "";
+ const result = tryParseJSON(str);
+
+ expect(result).to.equal(null);
+ });
+
+ test("check tryParseJSON with invalid string", () => {
+ const str = "invalid";
+ const result = tryParseJSON(str);
+
+ expect(result).to.equal(null);
+ });
+
+ test("check tryParseJSON with null", () => {
+ const result = tryParseJSON(null as any);
+
+ expect(result).to.equal(null);
+ });
+
+ test("check buildLambdaHttpResponse with default params", () => {
+ const result = buildLambdaHttpResponse();
+
+ expect(result).to.deep.equal({
+ statusCode: 200,
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Credentials": true,
+ },
+ body: JSON.stringify(
+ {
+ status: true,
+ statusMessage: "Success",
+ data: null,
+ },
+ null,
+ 2
+ ),
+ });
+ });
+
+ test("check buildLambdaHttpResponse with custom params", () => {
+ const result = buildLambdaHttpResponse({
+ headers: { "Content-Type": "application/json" },
+ statusCode: 400,
+ status: false,
+ statusMessage: "Bad Request",
+ data: { key: "value" },
+ });
+
+ expect(result).to.deep.equal({
+ statusCode: 400,
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Credentials": true,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(
+ {
+ status: false,
+ statusMessage: "Bad Request",
+ data: { key: "value" },
+ },
+ null,
+ 2
+ ),
+ });
+ });
+});
diff --git a/source/packages/services/sample-service/test/jest/utils/logger.spec.ts b/source/packages/services/sample-service/test/jest/utils/logger.spec.ts
new file mode 100644
index 0000000..66b4301
--- /dev/null
+++ b/source/packages/services/sample-service/test/jest/utils/logger.spec.ts
@@ -0,0 +1,8 @@
+import { expect } from "chai";
+import { logger } from "../../../src/utils/logger";
+
+describe("Logger tests", () => {
+ test("check logger", () => {
+ expect(logger).to.not.equal(undefined);
+ });
+});
\ No newline at end of file
diff --git a/source/packages/services/sample-service/tsconfig.json b/source/packages/services/sample-service/tsconfig.json
new file mode 100644
index 0000000..d579232
--- /dev/null
+++ b/source/packages/services/sample-service/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": ".",
+ "lib": [
+ "es2020"
+ ],
+ "typeRoots": [
+ "./node_modules/@types"
+ ],
+ "resolveJsonModule": true,
+ "esModuleInterop": true
+ },
+ "include": [ "./" ],
+ "exclude": [
+ "node_modules",
+ "cdk.out",
+ "../node_modules",
+ "dist",
+ ".vscode",
+ ".git",
+ "./**/__mocks__/*.ts",
+ "test",
+ ]
+}
diff --git a/source/rush.json b/source/rush.json
new file mode 100644
index 0000000..913905d
--- /dev/null
+++ b/source/rush.json
@@ -0,0 +1,46 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json",
+ "rushVersion": "5.100.1",
+ "pnpmVersion": "8.6.2",
+ "projects": [
+ {
+ "packageName": "@hanhdt/sample-service",
+ "projectFolder": "packages/services/sample-service",
+ "reviewCategory": "production",
+ "shouldPublish": true
+ },
+ {
+ "packageName": "@hanhdt/logger",
+ "projectFolder": "packages/libraries/core/logger",
+ "reviewCategory": "library",
+ "shouldPublish": false
+ },
+ {
+ "packageName": "@hanhdt/config-inject",
+ "projectFolder": "packages/libraries/core/config-inject",
+ "reviewCategory": "library",
+ "shouldPublish": false
+ }
+ ],
+ "pnpmOptions": {
+ "strictPeerDependencies": false,
+ "preventManualShrinkwrapChanges": true
+ },
+ "nodeSupportedVersionRange": ">=16.0.0 <20.0.0",
+ "projectFolderMinDepth": 2,
+ "projectFolderMaxDepth": 4,
+ "gitPolicy": {},
+ "repository": {
+ "url": "https://github.com/hanhdt/ts-microservices-rush-boilerplate.git",
+ "defaultBranch": "develop",
+ "defaultRemote": "origin"
+ },
+ "eventHooks": {
+ "preRushInstall": [],
+ "postRushInstall": [],
+ "preRushBuild": [],
+ "postRushBuild": []
+ },
+ "variants": [],
+ "telemetryEnabled": false
+}
diff --git a/source/tsconfig.base.json b/source/tsconfig.base.json
new file mode 100644
index 0000000..d1ae9f2
--- /dev/null
+++ b/source/tsconfig.base.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "commonjs",
+
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+
+ "strict": true,
+ "strictNullChecks": true,
+
+ "allowSyntheticDefaultImports": true,
+ "allowUnreachableCode": false,
+ "allowUnusedLabels": false,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "noImplicitAny": true,
+ "noImplicitReturns": true,
+ "noImplicitUseStrict": false,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": false,
+ "pretty": true,
+ "removeComments": true,
+ }
+}
\ No newline at end of file
diff --git a/source/tsconfig.json b/source/tsconfig.json
new file mode 100644
index 0000000..e9ce6ad
--- /dev/null
+++ b/source/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.base.json",
+ "files": [],
+ "references": [
+ {
+ "path": "./packages/**"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/source/tslint-base.json b/source/tslint-base.json
new file mode 100644
index 0000000..9fc5ad9
--- /dev/null
+++ b/source/tslint-base.json
@@ -0,0 +1,115 @@
+{
+ "rules": {
+ "no-empty-interface": true,
+ "no-internal-module": true,
+ "no-namespace": true,
+ "no-reference": true,
+ "no-var-requires": true,
+ "only-arrow-functions": [
+ true,
+ "allow-named-functions"
+ ],
+ "prefer-for-of": true,
+ "unified-signatures": true,
+
+ // "await-promise": true,
+ "curly": true,
+ "forin": true,
+ "no-arg": true,
+ "no-conditional-assignment": true,
+ "no-construct": true,
+ "no-empty": true,
+ "no-eval": true,
+ // "no-floating-promises": true,
+ // "no-for-in-array": true,
+ "no-invalid-this": true,
+ "no-misused-new": true,
+ "no-shadowed-variable": true,
+ "no-string-throw": true,
+ "no-switch-case-fall-through": true,
+ // "no-unbound-method": true,
+ // "no-unsafe-any": true,
+ "no-unused-expression": true,
+ // "no-unused-variable": true,
+ // "no-use-before-declare": true,
+ "no-var-keyword": true,
+ // "no-void-expression": true,
+ "radix": true,
+ // "restrict-plus-operands": true,
+ // "strict-type-predicates": true,
+ "switch-default": true,
+ "triple-equals": true,
+ "use-isnan": true,
+
+ "eofline": true,
+ "indent": [
+ true,
+ "tab"
+ ],
+ "no-default-export": true,
+ "no-require-imports": false,
+ "no-trailing-whitespace": true,
+ "prefer-const": true,
+ "trailing-comma": [
+ false
+ ],
+
+ "array-type": [
+ true,
+ "array"
+ ],
+ "arrow-return-shorthand": true,
+ "callable-types": true,
+ "class-name": true,
+ "comment-format": [
+ true,
+ "check-space"
+ // "check-uppercase"
+ ],
+ "import-spacing": true,
+ "new-parens": true,
+ "no-boolean-literal-compare": false,
+ "no-consecutive-blank-lines": [
+ true
+ ],
+ "no-unnecessary-initializer": true,
+ // "no-unnecessary-qualifier": true,
+ "object-literal-shorthand": true,
+ "one-line": [
+ true,
+ "check-catch",
+ "check-finally",
+ "check-else",
+ "check-open-brace",
+ "check-whitespace"
+ ],
+ "one-variable-per-declaration": [
+ true
+ ],
+ "prefer-method-signature": true,
+ "quotemark": [
+ true,
+ "single"
+ ],
+ "semicolon": [
+ true,
+ "always"
+ ],
+ "space-before-function-paren": false,
+ "variable-name": [
+ true,
+ "ban-keywords"
+ ],
+ "whitespace": [
+ false,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-module",
+ "check-separator",
+ "check-type",
+ "check-typecast",
+ "check-preblock"
+ ]
+ }
+}
\ No newline at end of file