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