├── .gitignore ├── .np-config.json ├── .npmignore ├── README.md ├── SUMMARY.md ├── bang-zhu-yu-jian-yi.md ├── fluttergitlab-gong-neng └── fluttergitlab-ji-ben-jie-shao.md ├── nestjsgeneral-gong-neng └── nestsgeneral-ji-ben-jie-shao.md ├── package.json ├── src ├── CliOptions.ts ├── TemplateConfig.ts ├── index.ts ├── templates │ ├── flutter-gitlab │ │ ├── .gitlab-ci.yml │ │ ├── android │ │ │ └── fastlane │ │ │ │ ├── Appfile │ │ │ │ ├── Fastfile │ │ │ │ └── README.md │ │ ├── ios │ │ │ └── fastlane │ │ │ │ ├── Appfile │ │ │ │ ├── Fastfile │ │ │ │ └── README.md │ │ └── lib │ │ │ ├── env.dart │ │ │ ├── main_prod.dart │ │ │ └── main_staging.dart │ └── nestjs-general │ │ ├── .dockerignore │ │ ├── .env │ │ ├── development.env │ │ ├── production.env │ │ ├── provision.env │ │ └── test.env │ │ ├── .github │ │ └── workflows │ │ │ └── blank.yml │ │ ├── .gitignore │ │ ├── .np-config.json │ │ ├── .prettierrc │ │ ├── .vscode │ │ └── launch.json │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── bitbucket-pipelines.yml │ │ ├── cdk-k8s │ │ ├── .gitignore │ │ ├── __snapshots__ │ │ │ └── main.test.ts.snap │ │ ├── cdk8s.yaml │ │ ├── help │ │ ├── imports │ │ │ └── k8s.ts │ │ ├── jest.config.js │ │ ├── main.test.ts │ │ ├── main.ts │ │ ├── package-lock.json │ │ ├── package.json │ │ └── tsconfig.json │ │ ├── cloudbuild.yaml │ │ ├── devspace.yaml │ │ ├── docker-compose.yml │ │ ├── nest-cli.json │ │ ├── nodemon.json │ │ ├── package.json │ │ ├── src │ │ ├── api-response │ │ │ ├── api-response.service.spec.ts │ │ │ └── api-response.service.ts │ │ ├── apiv1 │ │ │ ├── apiv1.module.ts │ │ │ └── user │ │ │ │ ├── user.controller.spec.ts │ │ │ │ └── user.controller.ts │ │ ├── apiv2 │ │ │ ├── apiv2.module.ts │ │ │ └── user │ │ │ │ ├── user.controller.spec.ts │ │ │ │ └── user.controller.ts │ │ ├── app.controller.spec.ts │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ ├── auth │ │ │ ├── auth.module.ts │ │ │ ├── auth.service.spec.ts │ │ │ ├── auth.service.ts │ │ │ ├── domain │ │ │ │ ├── jwt.strategy.ts │ │ │ │ └── strategy.module.ts │ │ │ └── jwt-auth.guard.ts │ │ ├── config │ │ │ ├── config.module.ts │ │ │ └── config.service.ts │ │ ├── decorators │ │ │ ├── files │ │ │ │ ├── csv-validator │ │ │ │ │ ├── csv-validator.ts │ │ │ │ │ └── field-validator.ts │ │ │ │ ├── upload-csv-file.decorator.ts │ │ │ │ ├── upload-image.decorator.ts │ │ │ │ ├── upload-pdf-file.decorator.ts │ │ │ │ └── upload-svg-file.decorator.ts │ │ │ ├── inject-winston-logger-service.decorator.ts │ │ │ └── tracking-id.decorator.ts │ │ ├── elasticsearch-config │ │ │ ├── elasticsearch-config.module.ts │ │ │ ├── elasticsearch-config.service.spec.ts │ │ │ └── elasticsearch-config.service.ts │ │ ├── enum │ │ │ ├── account-type.ts │ │ │ ├── api-business-code.ts │ │ │ ├── file-extension-names.ts │ │ │ └── gs-search-type.ts │ │ ├── errors │ │ │ ├── multer-transform-exception.ts │ │ │ └── service-exceptions.interface.ts │ │ ├── events │ │ │ ├── events.gateway.spec.ts │ │ │ ├── events.gateway.ts │ │ │ └── events.module.ts │ │ ├── filter │ │ │ ├── http-filter.filter.spec.ts │ │ │ └── http-filter.filter.ts │ │ ├── interface │ │ │ ├── base-context.interface.ts │ │ │ ├── multer-file.interface.ts │ │ │ ├── user-id.interface.ts │ │ │ └── user │ │ │ │ ├── query │ │ │ │ ├── user-query.ts │ │ │ │ └── user-signin-data-query.interface copy.ts │ │ │ │ └── response │ │ │ │ └── user-signin.interface.ts │ │ ├── main.ts │ │ ├── middleware │ │ │ ├── http-logger.middleware.spec.ts │ │ │ └── http-logger.middleware.ts │ │ ├── multer │ │ │ ├── multer-config.service.ts │ │ │ └── multer.module.ts │ │ ├── pipe │ │ │ ├── date-time.pipe.ts │ │ │ ├── general-validation.pipe.spec.ts │ │ │ ├── general-validation.pipe.ts │ │ │ ├── integer.pipe.ts │ │ │ └── string.pipe.ts │ │ ├── redis │ │ │ └── redis-client │ │ │ │ ├── redis-client.module.ts │ │ │ │ ├── redis-client.service.spec.ts │ │ │ │ └── redis-client.service.ts │ │ ├── services │ │ │ ├── date-time │ │ │ │ ├── date-time.service.spec.ts │ │ │ │ └── date-time.service.ts │ │ │ ├── service.module.ts │ │ │ └── user │ │ │ │ ├── user.service.spec.ts │ │ │ │ └── user.service.ts │ │ ├── system-mailer │ │ │ ├── mailer-config │ │ │ │ └── mailer-config.service.ts │ │ │ ├── system-mailer.module.ts │ │ │ └── system-mailer.service.ts │ │ ├── type-orm-config │ │ │ ├── type-orm-config.module.ts │ │ │ ├── type-orm-config.service.spec.ts │ │ │ └── type-orm-config.service.ts │ │ └── winston │ │ │ ├── __mocks__ │ │ │ └── logger-helper.service.ts │ │ │ ├── appwinston.module.ts │ │ │ └── logger-helper.service.ts │ │ ├── test │ │ ├── app.e2e-spec.ts │ │ ├── jest-e2e.json │ │ ├── mock-test-service.module.ts │ │ ├── mock-test.module.ts │ │ └── mockRepository.ts │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ ├── tslint.json │ │ ├── webpack.config.js │ │ └── yarn.lock └── utils │ └── template.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /node_modules 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # OS 13 | .DS_Store 14 | 15 | # Tests 16 | /coverage 17 | /.nyc_output 18 | 19 | # IDEs and editors 20 | /.idea 21 | .project 22 | .classpath 23 | .c9/ 24 | *.launch 25 | .settings/ 26 | *.sublime-workspace 27 | 28 | # IDE - VSCode 29 | .vscode/* 30 | !.vscode/settings.json 31 | !.vscode/tasks.json 32 | !.vscode/launch.json 33 | !.vscode/extensions.json 34 | src/coverage 35 | swagger-spec-v1.json 36 | 37 | 38 | # Ignore DevSpace cache and log folder 39 | .devspace/ 40 | 41 | dist/ 42 | 43 | test/ 44 | report.xml 45 | -------------------------------------------------------------------------------- /.np-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "publishConfig": { 3 | "access": "public", 4 | "registry": "https://registry.npmjs.org" 5 | } 6 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | report.xml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 本篇文章將介紹如何快速開始 yNest 3 | --- 4 | 5 | # 快速開始 6 | 7 | ## 基本介紹 8 | 9 | ynest, 是一個幫助項目開始的腳手架,目前包含了 nest.js、flutter 項目,內置 CI/CD 等自動化腳本設定。 10 | 11 | [文件位置](https://yasuoyuhao.gitbook.io/ynest) 12 | [原始碼位置](https://github.com/yasuoyuhao/ynest) 13 | 14 | ## 基本指令 15 | 16 | 請使用以下指令快速開始: 17 | 18 | ```bash 19 | yarn create @klearthinkk/ynest 20 | ``` 21 | 22 | 或者: 23 | 24 | ```bash 25 | npx @klearthinkk/create-ynest 26 | ``` 27 | 28 | 接下來,你會看到以下選項: 29 | 30 | ```text 31 | What project template would you like to generate? (Use arrow keys) 32 | ❯ flutter-gitlab 33 | nestjs-general 34 | ``` 35 | 36 | * `flutter-gitlab` 可以快速啟動一個 `flutter App`,其中包含了自動化 CI/CD,以及企業級的環境配置方式,詳細用法可以參考[這裡](https://www.appcoda.com.tw/flutter-app-%E7%92%B0%E5%A2%83%E7%AE%A1%E7%90%86/)。 37 | * `nestjs-general` 可以快速啟動與建置一個 `nest.js App`,其中包含了自動化 CI/CD、容器化設定、TypeORM 配置、Elasticsearch、Redis、RabbitMQ與企業級的環境配置方式。 38 | 39 | 詳細內容請參考[文件](https://yasuoyuhao.gitbook.io/ynest) 40 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [快速開始](README.md) 4 | 5 | ## nestjs-general 功能 6 | 7 | * [Nests-General 基本介紹](nestjsgeneral-gong-neng/nestsgeneral-ji-ben-jie-shao.md) 8 | 9 | ## flutter-gitlab 功能 10 | 11 | * [Flutter-Gitlab 基本介紹](fluttergitlab-gong-neng/fluttergitlab-ji-ben-jie-shao.md) 12 | 13 | --- 14 | 15 | * [幫助與建議](bang-zhu-yu-jian-yi.md) 16 | 17 | -------------------------------------------------------------------------------- /bang-zhu-yu-jian-yi.md: -------------------------------------------------------------------------------- 1 | # 幫助與建議 2 | 3 | ## 如果有任何問題或是建議歡迎使用下列聯絡方式: 4 | 5 | {% hint style="info" %} 6 | 郵件地址:yasuoyuhao@klearthink.com 7 | {% endhint %} 8 | 9 | -------------------------------------------------------------------------------- /fluttergitlab-gong-neng/fluttergitlab-ji-ben-jie-shao.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 本篇文章將介紹 flutter-gitlab 的基本概念 3 | --- 4 | 5 | # Flutter-Gitlab 基本介紹 6 | 7 | ## 基本概念 8 | 9 | `Flutter-Gitlab` 是基於 [`Flutter`](https://github.com/flutter/flutter) 項目所開發的快速建置模板,其中包含 `Gitlab CI` 配置,可以快速建立 `iOS` 以及 `Android` 的自動化 CI/CD 流程,詳細教學可以查看[這篇文章](https://www.appcoda.com.tw/flutter-app-%E7%92%B0%E5%A2%83%E7%AE%A1%E7%90%86/)。 10 | 11 | ## 要求 12 | 13 | * `需安裝 flutter sdk`,[安裝位置](https://flutter.dev/docs/get-started/install?gclid=CjwKCAiAo5qABhBdEiwAOtGmbmydxDX70Dm4GvzoBQNwIbW2L3BY1jVqK71-JN3m2LHK86YT8pLjShoCDfwQAvD_BwE&gclsrc=aw.ds) 14 | * `node version >= 12` 15 | 16 | ## 檔案結構 17 | 18 | ├── android 19 | 20 | │ └── fastlane - Android fastlane 配置 21 | 22 | │ ├── Appfile 23 | 24 | │ ├── Fastfile 25 | 26 | │ └── README.md 27 | 28 | ├── ios - ios fastlane 配置 29 | 30 | │ └── fastlane 31 | 32 | │ ├── Appfile 33 | 34 | │ ├── Fastfile 35 | 36 | │ └── README.md 37 | 38 | └── lib - 不同環境的入口配置 39 | 40 | ├── env.dart 41 | 42 | ├── main\_prod.dart 43 | 44 | └── main\_staging.dart 45 | 46 | -------------------------------------------------------------------------------- /nestjsgeneral-gong-neng/nestsgeneral-ji-ben-jie-shao.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 本篇文章將介紹 nests-general 的基本概念 3 | --- 4 | 5 | # Nests-General 基本介紹 6 | 7 | ## 基本概念 8 | 9 | `nestjs-general` 專案樣板源自於 [`nest.js` 項目](https://github.com/nestjs/nest),在企業級應用基礎上,整理出常用的模組與設定。 10 | 11 | > Nest是一個有用的高效,可擴展的Node.js服務器端應用程序的框架。它使用漸進式JavaScript,內置並完全支持TypeScript(但仍允許開發人員使用純JavaScript編寫代碼)並結合了OOP(面向對象編程),FP(函數式編程)和FRP(函數式響應編程)的元素。 12 | > 13 | > 在這些框架之上提供了一定程度的抽象,同時也將其 API 直接暴露給開發人員。這樣可以輕鬆使用每個平台的無數第三方模塊。 14 | 15 | ## 要求 16 | 17 | * `node version >= 12` 18 | * 如果使用 `docker` 功能,需安裝 `docker & docker-compose` 19 | 20 | ## 檔案結構 21 | 22 | ├── Dockerfile 23 | 24 | ├── README.md 25 | 26 | ├── bitbucket-pipelines.yml - bitbucket 的 pipelines 設置 27 | 28 | ├── cdk-k8s - 協助您撰寫 K8s yaml 部署 29 | 30 | ├── cloudbuild.yaml - Google Cloud Build 配置 31 | 32 | ├── devspace.yaml - devspace 配置 33 | 34 | ├── docker-compose.yml - docker-compose 配置 35 | 36 | ├── src - 主要程式碼位置 37 | 38 | │ ├── api-response - 共用的 API Response 服務與結構 39 | 40 | │ ├── apiv1 - 版本 1 的 API 41 | 42 | │ ├── apiv2 - 版本 2 的 API 43 | 44 | │ ├── auth - 鑑權服務 45 | 46 | │ ├── config - 環境配置設定,通常搭配 .env 資料夾中變數 47 | 48 | │ ├── decorators - 常用的裝飾器 49 | 50 | │ ├── elasticsearch-config - elasticsearch的連接設置 51 | 52 | │ ├── enum - 常用的enum 53 | 54 | │ ├── errors - 常用的error 55 | 56 | │ ├── events - websocket 服務 57 | 58 | │ ├── filter - http 攔截器服務 59 | 60 | │ ├── interface - 常用的interface 61 | 62 | │ ├── main.ts │ ├── middleware - http 中間件服務 63 | 64 | │ ├── multer - multer 上傳檔案配置 65 | 66 | │ ├── pipe - 常用的 pipe,通常用於資料驗證 67 | 68 | │ ├── redis - redis 連線配置 69 | 70 | │ ├── services - services 邏輯服務層主要位置 71 | 72 | │ ├── system-mailer - Email 信件服務 73 | 74 | │ ├── type-orm-config - TypeORM 連線配置服務 75 | 76 | │ └── winston - TypeORM 日誌服務 77 | 78 | ├── test - 常用的測試元件 79 | 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@klearthinkk/create-ynest", 3 | "version": "1.1.2", 4 | "description": "new nest.js project from klearthinkk", 5 | "keywords": [ 6 | "nestjs", 7 | "flutter", 8 | "typescript", 9 | "dart", 10 | "ios", 11 | "android", 12 | "node", 13 | "kubernetes", 14 | "k8s", 15 | "docker", 16 | "container", 17 | "devspace", 18 | "cdk8s", 19 | "bitbucket pipeline", 20 | "gitlab ci", 21 | "fastlane", 22 | "templates" 23 | ], 24 | "main": "dist/index.js", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/yasuoyuhao/ynest" 28 | }, 29 | "author": "yasuoyuhao ", 30 | "bin": { 31 | "create-ynest": "./dist/index.js" 32 | }, 33 | "license": "MIT", 34 | "private": false, 35 | "scripts": { 36 | "start": "ts-node src/index.ts", 37 | "build": "tsc && shx rm -rf dist/templates && shx cp -r src/templates dist", 38 | "release": "yarn build && np" 39 | }, 40 | "devDependencies": { 41 | "@types/ejs": "^3.0.5", 42 | "@types/inquirer": "^7.3.1", 43 | "@types/node": "^14.14.10", 44 | "@types/shelljs": "^0.8.8", 45 | "@types/yargs": "^15.0.11", 46 | "np": "^7.0.0", 47 | "shx": "^0.3.3", 48 | "ts-node": "^9.1.0", 49 | "typescript": "^4.1.2" 50 | }, 51 | "dependencies": { 52 | "@types/chalk": "^2.2.0", 53 | "chalk": "^4.1.0", 54 | "ejs": "^3.1.5", 55 | "inquirer": "^7.3.3", 56 | "shelljs": "^0.8.4", 57 | "yargs": "^16.2.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/CliOptions.ts: -------------------------------------------------------------------------------- 1 | import { TemplateConfig } from './TemplateConfig'; 2 | 3 | 4 | export interface CliOptions { 5 | projectName: string; 6 | templateName: string; 7 | templatePath: string; 8 | tartgetPath: string; 9 | config: TemplateConfig; 10 | } 11 | -------------------------------------------------------------------------------- /src/TemplateConfig.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface TemplateConfig { 3 | files?: string[]; 4 | postMessage?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as inquirer from 'inquirer'; 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | import * as shell from 'shelljs'; 7 | import * as chalk from 'chalk'; 8 | import * as yargs from 'yargs'; 9 | import * as template from './utils/template'; 10 | import { TemplateConfig } from './TemplateConfig'; 11 | import { CliOptions } from './CliOptions'; 12 | 13 | const CHOICES = fs.readdirSync(path.join(__dirname, 'templates')); 14 | 15 | const QUESTIONS = [ 16 | { 17 | name: 'template', 18 | type: 'list', 19 | message: 'What project template would you like to generate?', 20 | choices: CHOICES, 21 | when: () => !yargs.argv['template'] 22 | }, 23 | { 24 | name: 'name', 25 | type: 'input', 26 | message: 'Project name:', 27 | when: () => !yargs.argv['name'], 28 | validate: (input: string) => { 29 | if (/^([A-Za-z\-\_\d])+$/.test(input)) return true; 30 | else return 'Project name may only include letters, numbers, underscores and hashes.'; 31 | } 32 | } 33 | ]; 34 | 35 | const CURR_DIR = process.cwd(); 36 | 37 | inquirer.prompt(QUESTIONS) 38 | .then(answers => { 39 | 40 | answers = Object.assign({}, answers, yargs.argv); 41 | 42 | const projectChoice = answers['template']; 43 | const projectName = answers['name']; 44 | const templatePath = path.join(__dirname, 'templates', projectChoice); 45 | const tartgetPath = path.join(CURR_DIR, projectName); 46 | const templateConfig = getTemplateConfig(templatePath); 47 | 48 | const options: CliOptions = { 49 | projectName, 50 | templateName: projectChoice, 51 | templatePath, 52 | tartgetPath, 53 | config: templateConfig 54 | } 55 | 56 | if (!createProject(tartgetPath)) { 57 | return; 58 | } 59 | 60 | createDirectoryContents(templatePath, projectName, templateConfig); 61 | 62 | if (!postProcess(options)) { 63 | shell.rm('-rf', options.tartgetPath); 64 | return; 65 | } 66 | 67 | showMessage(options); 68 | }); 69 | 70 | function showMessage(options: CliOptions) { 71 | console.log(''); 72 | console.log(chalk.green('Done.')); 73 | console.log(chalk.green(`Go into the project: cd ${options.projectName}`)); 74 | 75 | const message = options.config.postMessage; 76 | 77 | if (message) { 78 | console.log(''); 79 | console.log(chalk.yellow(message)); 80 | console.log(''); 81 | } 82 | 83 | } 84 | 85 | function getTemplateConfig(templatePath: string): TemplateConfig { 86 | const configPath = path.join(templatePath, '.template.json'); 87 | 88 | if (!fs.existsSync(configPath)) return {}; 89 | 90 | const templateConfigContent = fs.readFileSync(configPath); 91 | 92 | if (templateConfigContent) { 93 | return JSON.parse(templateConfigContent.toString()); 94 | } 95 | 96 | return {}; 97 | } 98 | 99 | function createProject(projectPath: string) { 100 | 101 | if (fs.existsSync(projectPath)) { 102 | console.log(chalk.red(`Folder ${projectPath} exists. Delete or use another name.`)); 103 | return false; 104 | } 105 | 106 | fs.mkdirSync(projectPath); 107 | return true; 108 | } 109 | 110 | function postProcess(options: CliOptions) { 111 | if (isNode(options)) { 112 | return postProcessNode(options); 113 | } 114 | 115 | if (isDart(options)) { 116 | return postProcessDart(options); 117 | } 118 | 119 | return true; 120 | } 121 | 122 | function isNode(options: CliOptions) { 123 | return fs.existsSync(path.join(options.templatePath, 'package.json')); 124 | } 125 | 126 | function isDart(options: CliOptions) { 127 | return options.templateName === 'flutter-gitlab' 128 | } 129 | 130 | function postProcessNode(options: CliOptions) { 131 | shell.cd(options.tartgetPath); 132 | 133 | let cmd = ''; 134 | 135 | if (shell.which('yarn')) { 136 | cmd = 'yarn'; 137 | } else if (shell.which('npm')) { 138 | cmd = 'npm install'; 139 | } 140 | 141 | if (fs.existsSync('cdk-k8s')) { 142 | cmd += ' && cd cdk-k8s && yarn' 143 | } 144 | 145 | if (cmd) { 146 | const result = shell.exec(cmd); 147 | 148 | if (result.code !== 0) { 149 | return false; 150 | } 151 | } else { 152 | console.log(chalk.red('No yarn or npm found. Cannot run installation.')); 153 | } 154 | 155 | return true; 156 | } 157 | 158 | function postProcessDart(options: CliOptions) { 159 | 160 | let cmd = ''; 161 | 162 | if (!shell.which('flutter')) { 163 | console.log(chalk.red('No flutter found. Cannot run installation.')); 164 | 165 | return false; 166 | } 167 | cmd += `flutter create ${options.projectName}` 168 | 169 | if (!shell.which('fastlane')) { 170 | console.log(chalk.red('No fastlane found. Cannot run installation.')); 171 | 172 | return false; 173 | } 174 | 175 | const result = shell.exec(cmd); 176 | 177 | if (result.code !== 0) { 178 | return false; 179 | } 180 | 181 | return true; 182 | } 183 | 184 | const SKIP_FILES = ['node_modules', '.template.json']; 185 | 186 | function createDirectoryContents(templatePath: string, projectName: string, config: TemplateConfig) { 187 | const filesToCreate = fs.readdirSync(templatePath); 188 | 189 | filesToCreate.forEach(file => { 190 | const origFilePath = path.join(templatePath, file); 191 | 192 | // get stats about the current file 193 | const stats = fs.statSync(origFilePath); 194 | 195 | if (SKIP_FILES.indexOf(file) > -1) return; 196 | 197 | if (stats.isFile()) { 198 | let contents = fs.readFileSync(origFilePath, 'utf8'); 199 | 200 | contents = template.render(contents, { projectName }); 201 | 202 | const writePath = path.join(CURR_DIR, projectName, file); 203 | fs.writeFileSync(writePath, contents, 'utf8'); 204 | } else if (stats.isDirectory()) { 205 | fs.mkdirSync(path.join(CURR_DIR, projectName, file)); 206 | 207 | // recursive call 208 | createDirectoryContents(path.join(templatePath, file), path.join(projectName, file), config); 209 | } 210 | }); 211 | } 212 | -------------------------------------------------------------------------------- /src/templates/flutter-gitlab/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - test_coverage 3 | - unit_tests 4 | - develop 5 | - master 6 | 7 | variables: 8 | LANG: "en_US.UTF-8" 9 | LC_ALL: "en_US.UTF-8" 10 | GIT_SUBMODULE_STRATEGY: normal 11 | GIT_DEPTH: 0 12 | 13 | test_coverage: 14 | stage: test_coverage 15 | before_script: 16 | - flutter pub get 17 | - flutter clean 18 | - flutter --version 19 | 20 | script: 21 | - flutter test --coverage 22 | - lcov --list coverage/lcov.info 23 | - genhtml coverage/lcov.info --output=coverage 24 | artifacts: 25 | paths: 26 | - coverage 27 | expire_in: 10 days 28 | tags: 29 | - flutter 30 | only: 31 | - merge_requests 32 | - develop 33 | 34 | unit_tests_iOS: 35 | tags: 36 | - flutter 37 | stage: unit_tests 38 | script: 39 | - cd ios && fastlane test --verbose 40 | only: 41 | - branches 42 | except: 43 | - tags 44 | - /^release*/ 45 | - release 46 | allow_failure: false 47 | 48 | unit_tests_android: 49 | tags: 50 | - flutter 51 | stage: unit_tests 52 | script: 53 | - cd android && fastlane test --verbose 54 | only: 55 | - branches 56 | except: 57 | - tags 58 | - /^release*/ 59 | - release 60 | allow_failure: false 61 | 62 | develop_iOS: 63 | tags: 64 | - flutter 65 | stage: develop 66 | script: 67 | - cd ios && fastlane beta --verbose 68 | only: 69 | - /^develop-.*/ 70 | - develop 71 | environment: 72 | name: develop 73 | 74 | develop_android: 75 | tags: 76 | - flutter 77 | stage: develop 78 | script: 79 | - cd android && fastlane beta --verbose 80 | only: 81 | - /^develop-.*/ 82 | - develop 83 | environment: 84 | name: develop_android 85 | 86 | developrelease_iOS: 87 | tags: 88 | - flutter 89 | stage: develop 90 | script: 91 | - cd ios && fastlane betagitlab --verbose 92 | only: 93 | - /^release*/ 94 | - release 95 | environment: 96 | name: develop 97 | 98 | developrelease_android: 99 | tags: 100 | - flutter 101 | stage: develop 102 | script: 103 | - cd android && fastlane betagitlab --verbose 104 | only: 105 | - /^release*/ 106 | - release 107 | environment: 108 | name: develop_android 109 | 110 | master_iOS: 111 | tags: 112 | - flutter 113 | stage: master 114 | script: 115 | - cd ios && fastlane release --verbose 116 | only: 117 | - master 118 | environment: 119 | name: production 120 | 121 | master_android: 122 | tags: 123 | - flutter 124 | stage: master 125 | script: 126 | - cd android && fastlane release --verbose 127 | only: 128 | - master 129 | environment: 130 | name: production_android 131 | 132 | masterrelease_iOS: 133 | tags: 134 | - flutter 135 | stage: master 136 | script: 137 | - cd ios && fastlane releasegitlab --verbose 138 | only: 139 | - /^release*/ 140 | - release 141 | environment: 142 | name: production 143 | 144 | masterrelease_android: 145 | tags: 146 | - flutter 147 | stage: master 148 | script: 149 | - cd android && fastlane releasegitlab --verbose 150 | only: 151 | - /^release*/ 152 | - release 153 | environment: 154 | name: production_android -------------------------------------------------------------------------------- /src/templates/flutter-gitlab/android/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | json_key_file("./api-6575797148355186220-158009-9aa0b7ef8416.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one 2 | package_name("com.klearthink.xiaochengeatting") # e.g. com.krausefx.app 3 | 4 | for_platform :android do 5 | for_lane :beta do 6 | package_name "com.klearthink.xiaochengeatting.staging" 7 | end 8 | end -------------------------------------------------------------------------------- /src/templates/flutter-gitlab/android/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | default_platform(:android) 17 | 18 | platform :android do 19 | 20 | desc "Build and Unit Test" 21 | lane :test do 22 | # Return the number of commits in current git branch 23 | build_number = number_of_commits(all: true) 24 | Dir.chdir "../.." do 25 | sh("flutter", "packages", "get") 26 | sh("flutter", "clean") 27 | sh("flutter", "build", "apk", "-t", "lib/main_staging.dart", "--flavor=Staging", "--build-number=#{build_number}") 28 | end 29 | end 30 | 31 | desc "Submit a new build to apk" 32 | lane :betabuild do 33 | # Return the number of commits in current git branch 34 | build_number = number_of_commits(all: true) 35 | ci_pipeline_id = ENV['CI_PIPELINE_ID'] 36 | if !ci_pipeline_id.nil? && !ci_pipeline_id.empty? 37 | build_number = ci_pipeline_id 38 | end 39 | Dir.chdir "../.." do 40 | sh("flutter", "packages", "get") 41 | sh("flutter", "clean") 42 | sh("flutter", "build", "appbundle", "-t", "lib/main_staging.dart", "--flavor=Staging", "--build-number=#{build_number}") 43 | end 44 | end 45 | 46 | desc "Submit a new build to apk" 47 | lane :beta do 48 | # Return the number of commits in current git branch 49 | build_number = number_of_commits(all: true) 50 | ci_pipeline_id = ENV['CI_PIPELINE_ID'] 51 | if !ci_pipeline_id.nil? && !ci_pipeline_id.empty? 52 | build_number = ci_pipeline_id 53 | end 54 | Dir.chdir "../.." do 55 | sh("flutter", "packages", "get") 56 | sh("flutter", "clean") 57 | sh("flutter", "build", "appbundle", "-t", "lib/main_staging.dart", "--flavor=Staging", "--build-number=#{build_number}") 58 | end 59 | supply(track: "internal", aab:"../build/app/outputs/bundle/StagingRelease/app-Staging-release.aab", rollout: "1.0", 60 | skip_upload_images: true, 61 | skip_upload_screenshots: true, 62 | skip_upload_metadata: true,) 63 | end 64 | 65 | desc "Push a new beta build to apk" 66 | lane :betagitlab do 67 | # Return the number of commits in current git branch 68 | build_number = number_of_commits(all: true) 69 | ci_pipeline_id = ENV['CI_PIPELINE_ID'] 70 | if !ci_pipeline_id.nil? && !ci_pipeline_id.empty? 71 | build_number = ci_pipeline_id 72 | end 73 | Dir.chdir "../.." do 74 | sh("flutter", "packages", "get") 75 | sh("flutter", "clean") 76 | sh("flutter", "build", "appbundle", "-t", "lib/main_staging.dart", "--flavor=Staging", "--build-number=#{build_number}") 77 | end 78 | 79 | supply(track: "internal", aab:"../build/app/outputs/bundle/StagingRelease/app-Staging-release.aab", rollout: "1.0", 80 | skip_upload_images: true, 81 | skip_upload_screenshots: true, 82 | skip_upload_metadata: true,) 83 | end 84 | 85 | desc "Submit a new build to Internal Track on Play" 86 | lane :releasebuild do 87 | # Return the number of commits in current git branch 88 | build_number = number_of_commits(all: true) 89 | ci_pipeline_id = ENV['CI_PIPELINE_ID'] 90 | if !ci_pipeline_id.nil? && !ci_pipeline_id.empty? 91 | build_number = ci_pipeline_id 92 | end 93 | Dir.chdir "../.." do 94 | sh("flutter", "packages", "get") 95 | sh("flutter", "clean") 96 | sh("flutter", "build", "appbundle", "-t", "lib/main_prod.dart", "--flavor=Prod", "--build-number=#{build_number}") 97 | end 98 | end 99 | 100 | desc "Submit a new build to Internal Track on Play" 101 | lane :release do 102 | # Return the number of commits in current git branch 103 | build_number = number_of_commits(all: true) 104 | ci_pipeline_id = ENV['CI_PIPELINE_ID'] 105 | if !ci_pipeline_id.nil? && !ci_pipeline_id.empty? 106 | build_number = ci_pipeline_id 107 | end 108 | Dir.chdir "../.." do 109 | sh("flutter", "packages", "get") 110 | sh("flutter", "clean") 111 | sh("flutter", "build", "appbundle", "-t", "lib/main_prod.dart", "--flavor=Prod", "--build-number=#{build_number}") 112 | end 113 | supply(track: "internal", aab:"../build/app/outputs/bundle/ProdRelease/app-Prod-release.aab", rollout: "1.0", 114 | skip_upload_images: true, 115 | skip_upload_screenshots: true, 116 | skip_upload_metadata: true,) 117 | end 118 | 119 | desc "Submit a new build to Internal Track on Play" 120 | lane :releasegitlab do 121 | # Return the number of commits in current git branch 122 | build_number = number_of_commits(all: true) 123 | ci_pipeline_id = ENV['CI_PIPELINE_ID'] 124 | if !ci_pipeline_id.nil? && !ci_pipeline_id.empty? 125 | build_number = ci_pipeline_id 126 | end 127 | Dir.chdir "../.." do 128 | sh("flutter", "packages", "get") 129 | sh("flutter", "clean") 130 | sh("flutter", "build", "apk", "-t", "lib/main_prod.dart", "--flavor=Prod", "--build-number=#{build_number}") 131 | end 132 | # supply(track: "internal", aab:"../build/app/outputs/bundle/release/app.aab", rollout: "1.0") 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /src/templates/flutter-gitlab/android/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew install fastlane` 16 | 17 | # Available Actions 18 | ## Android 19 | ### android test 20 | ``` 21 | fastlane android test 22 | ``` 23 | Build and Unit Test 24 | ### android betabuild 25 | ``` 26 | fastlane android betabuild 27 | ``` 28 | Submit a new build to apk 29 | ### android beta 30 | ``` 31 | fastlane android beta 32 | ``` 33 | Submit a new build to apk 34 | ### android betagitlab 35 | ``` 36 | fastlane android betagitlab 37 | ``` 38 | Push a new beta build to apk 39 | ### android releasebuild 40 | ``` 41 | fastlane android releasebuild 42 | ``` 43 | Submit a new build to Internal Track on Play 44 | ### android release 45 | ``` 46 | fastlane android release 47 | ``` 48 | Submit a new build to Internal Track on Play 49 | ### android releasegitlab 50 | ``` 51 | fastlane android releasegitlab 52 | ``` 53 | Submit a new build to Internal Track on Play 54 | 55 | ---- 56 | 57 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 58 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 59 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 60 | -------------------------------------------------------------------------------- /src/templates/flutter-gitlab/ios/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier("") # The bundle identifier of your app 2 | apple_id("") # Your Apple email address 3 | 4 | itc_team_id("") # App Store Connect Team ID 5 | team_id("") # Developer Portal Team ID 6 | 7 | for_platform :ios do 8 | for_lane :beta do 9 | app_identifier "-staging" 10 | end 11 | for_lane :betagitlab do 12 | app_identifier "-staging" 13 | end 14 | end 15 | # For more information about the Appfile, see: 16 | # https://docs.fastlane.tools/advanced/#appfile 17 | -------------------------------------------------------------------------------- /src/templates/flutter-gitlab/ios/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | default_platform(:ios) 17 | 18 | platform :ios do 19 | 20 | before_all do 21 | ENV["FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD"] = "" 22 | # ENV["FASTLANE_PASSWORD"] = "" 23 | sh("pod", "repo", "update") 24 | end 25 | 26 | desc "Build and Unit Test" 27 | lane :test do 28 | # Return the number of commits in current git branch 29 | build_number = number_of_commits(all: true) 30 | Dir.chdir "../.." do 31 | sh("flutter", "packages", "get") 32 | sh("flutter", "clean") 33 | sh("flutter", "build", "ios", "-t", "lib/main_staging.dart", "--flavor=Staging", "--no-codesign", "--build-number=#{ENV['CI_PIPELINE_ID']}") 34 | end 35 | end 36 | 37 | desc "Push a new beta build to TestFlight" 38 | lane :beta do 39 | # Return the number of commits in current git branch 40 | build_number = number_of_commits(all: true) 41 | copy_config_file("Staging") 42 | Dir.chdir "../.." do 43 | sh("flutter", "packages", "get") 44 | sh("flutter", "clean") 45 | sh("flutter", "build", "ios", "-t", "lib/main_staging.dart", "--flavor=Staging", "--no-codesign", "--build-number=#{ENV['CI_PIPELINE_ID']}") 46 | end 47 | sigh(force: true) 48 | build_app(workspace: "Runner.xcworkspace", scheme: "Staging", configuration: "Release-Staging") 49 | # upload_to_testflight 50 | pilot(skip_waiting_for_build_processing: true) 51 | # post_slack_message("Staging version #{ENV['CI_PIPELINE_ID']}") 52 | end 53 | 54 | desc "Push a new beta build to TestFlight" 55 | lane :betagitlab do 56 | # Return the number of commits in current git branch 57 | build_number = number_of_commits(all: true) 58 | copy_config_file("Staging") 59 | Dir.chdir "../.." do 60 | sh("flutter", "packages", "get") 61 | sh("flutter", "clean") 62 | sh("flutter", "build", "ios", "-t", "lib/main_staging.dart", "--flavor=Staging", "--no-codesign", "--build-number=#{ENV['CI_PIPELINE_ID']}") 63 | end 64 | sigh(force: true) 65 | build_app(workspace: "Runner.xcworkspace", scheme: "Staging", configuration: "Release-Staging") 66 | # upload_to_testflight 67 | pilot(skip_waiting_for_build_processing: true) 68 | # post_slack_message("Staging version #{ENV['CI_PIPELINE_ID']}") 69 | end 70 | 71 | desc "Push a new beta build to TestFlight" 72 | lane :release do 73 | # Return the number of commits in current git branch 74 | build_number = number_of_commits(all: true) 75 | copy_config_file("Prod") 76 | Dir.chdir "../.." do 77 | sh("flutter", "packages", "get") 78 | sh("flutter", "clean") 79 | sh("flutter", "build", "ios", "-t", "lib/main_prod.dart", "--flavor=Prod", "--no-codesign", "--build-number=#{ENV['CI_PIPELINE_ID']}") 80 | end 81 | sigh(force: true) 82 | build_app(workspace: "Runner.xcworkspace", scheme: "Prod", configuration: "Release-Prod") 83 | # upload_to_testflight 84 | pilot(skip_waiting_for_build_processing: true) 85 | # post_slack_message("Prod version #{ENV['CI_PIPELINE_ID']}") 86 | end 87 | 88 | desc "Push a new beta build to TestFlight" 89 | lane :releasegitlab do 90 | # Return the number of commits in current git branch 91 | build_number = number_of_commits(all: true) 92 | copy_config_file("Prod") 93 | Dir.chdir "../.." do 94 | sh("flutter", "packages", "get") 95 | sh("flutter", "clean") 96 | sh("flutter", "build", "ios", "-t", "lib/main_prod.dart", "--flavor=Prod", "--no-codesign", "--build-number=#{ENV['CI_PIPELINE_ID']}") 97 | end 98 | sigh(force: true) 99 | build_app(workspace: "Runner.xcworkspace", scheme: "Prod", configuration: "Release-Prod") 100 | # upload_to_testflight 101 | pilot(skip_waiting_for_build_processing: true) 102 | # post_slack_message("Prod version #{ENV['CI_PIPELINE_ID']}") 103 | end 104 | end 105 | 106 | def post_slack_message(message) 107 | slack( 108 | message: "App successfully uploaded to iTunesConnect. #{message}", 109 | success: true, 110 | slack_url: "https://hooks.slack.com/services/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" 111 | ) 112 | end 113 | 114 | def copy_config_file(env) 115 | sh("cp", "../Runner/GoogleServices/GoogleService-Info-#{env}.plist", "../Runner/GoogleService-Info.plist") 116 | end 117 | -------------------------------------------------------------------------------- /src/templates/flutter-gitlab/ios/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew install fastlane` 16 | 17 | # Available Actions 18 | ## iOS 19 | ### ios test 20 | ``` 21 | fastlane ios test 22 | ``` 23 | Build and Unit Test 24 | ### ios beta 25 | ``` 26 | fastlane ios beta 27 | ``` 28 | Push a new beta build to TestFlight 29 | ### ios betagitlab 30 | ``` 31 | fastlane ios betagitlab 32 | ``` 33 | Push a new beta build to TestFlight 34 | ### ios release 35 | ``` 36 | fastlane ios release 37 | ``` 38 | Push a new beta build to TestFlight 39 | ### ios releasegitlab 40 | ``` 41 | fastlane ios releasegitlab 42 | ``` 43 | Push a new beta build to TestFlight 44 | 45 | ---- 46 | 47 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 48 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 49 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 50 | -------------------------------------------------------------------------------- /src/templates/flutter-gitlab/lib/env.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | enum BuildFlavor { production, development, staging } 4 | 5 | BuildEnvironment get env => _env; 6 | BuildEnvironment _env; 7 | 8 | class BuildEnvironment { 9 | /// The backend server. 10 | final String baseUrl; 11 | final BuildFlavor flavor; 12 | 13 | BuildEnvironment._init({this.flavor, this.baseUrl}); 14 | 15 | /// Sets up the top-level [env] getter on the first call only. 16 | static void init({@required flavor, @required baseUrl}) => 17 | _env ??= BuildEnvironment._init(flavor: flavor, baseUrl: baseUrl); 18 | } -------------------------------------------------------------------------------- /src/templates/flutter-gitlab/lib/main_prod.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'env.dart'; 3 | 4 | void main() { 5 | BuildEnvironment.init( 6 | flavor: BuildFlavor.production, 7 | baseUrl: 8 | 'https://'); 9 | assert(env != null); 10 | runApp(MyApp()); 11 | } 12 | 13 | class MyApp extends StatelessWidget { 14 | // This widget is the root of your application. 15 | @override 16 | Widget build(BuildContext context) { 17 | return MaterialApp( 18 | title: 'Flutter Demo', 19 | theme: ThemeData( 20 | // This is the theme of your application. 21 | // 22 | // Try running your application with "flutter run". You'll see the 23 | // application has a blue toolbar. Then, without quitting the app, try 24 | // changing the primarySwatch below to Colors.green and then invoke 25 | // "hot reload" (press "r" in the console where you ran "flutter run", 26 | // or simply save your changes to "hot reload" in a Flutter IDE). 27 | // Notice that the counter didn't reset back to zero; the application 28 | // is not restarted. 29 | primarySwatch: Colors.blue, 30 | // This makes the visual density adapt to the platform that you run 31 | // the app on. For desktop platforms, the controls will be smaller and 32 | // closer together (more dense) than on mobile platforms. 33 | visualDensity: VisualDensity.adaptivePlatformDensity, 34 | ), 35 | home: MyHomePage(title: 'Flutter Demo Home Page'), 36 | ); 37 | } 38 | } 39 | 40 | class MyHomePage extends StatefulWidget { 41 | MyHomePage({Key key, this.title}) : super(key: key); 42 | 43 | // This widget is the home page of your application. It is stateful, meaning 44 | // that it has a State object (defined below) that contains fields that affect 45 | // how it looks. 46 | 47 | // This class is the configuration for the state. It holds the values (in this 48 | // case the title) provided by the parent (in this case the App widget) and 49 | // used by the build method of the State. Fields in a Widget subclass are 50 | // always marked "final". 51 | 52 | final String title; 53 | 54 | @override 55 | _MyHomePageState createState() => _MyHomePageState(); 56 | } 57 | 58 | class _MyHomePageState extends State { 59 | int _counter = 0; 60 | 61 | void _incrementCounter() { 62 | setState(() { 63 | // This call to setState tells the Flutter framework that something has 64 | // changed in this State, which causes it to rerun the build method below 65 | // so that the display can reflect the updated values. If we changed 66 | // _counter without calling setState(), then the build method would not be 67 | // called again, and so nothing would appear to happen. 68 | _counter++; 69 | }); 70 | } 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | // This method is rerun every time setState is called, for instance as done 75 | // by the _incrementCounter method above. 76 | // 77 | // The Flutter framework has been optimized to make rerunning build methods 78 | // fast, so that you can just rebuild anything that needs updating rather 79 | // than having to individually change instances of widgets. 80 | return Scaffold( 81 | appBar: AppBar( 82 | // Here we take the value from the MyHomePage object that was created by 83 | // the App.build method, and use it to set our appbar title. 84 | title: Text(widget.title), 85 | ), 86 | body: Center( 87 | // Center is a layout widget. It takes a single child and positions it 88 | // in the middle of the parent. 89 | child: Column( 90 | // Column is also a layout widget. It takes a list of children and 91 | // arranges them vertically. By default, it sizes itself to fit its 92 | // children horizontally, and tries to be as tall as its parent. 93 | // 94 | // Invoke "debug painting" (press "p" in the console, choose the 95 | // "Toggle Debug Paint" action from the Flutter Inspector in Android 96 | // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) 97 | // to see the wireframe for each widget. 98 | // 99 | // Column has various properties to control how it sizes itself and 100 | // how it positions its children. Here we use mainAxisAlignment to 101 | // center the children vertically; the main axis here is the vertical 102 | // axis because Columns are vertical (the cross axis would be 103 | // horizontal). 104 | mainAxisAlignment: MainAxisAlignment.center, 105 | children: [ 106 | Text( 107 | 'You have pushed the button this many times:', 108 | ), 109 | Text( 110 | '$_counter', 111 | style: Theme.of(context).textTheme.headline4, 112 | ), 113 | ], 114 | ), 115 | ), 116 | floatingActionButton: FloatingActionButton( 117 | onPressed: _incrementCounter, 118 | tooltip: 'Increment', 119 | child: Icon(Icons.add), 120 | ), // This trailing comma makes auto-formatting nicer for build methods. 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/templates/flutter-gitlab/lib/main_staging.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'env.dart'; 3 | 4 | void main() { 5 | BuildEnvironment.init( 6 | flavor: BuildFlavor.staging, 7 | baseUrl: 8 | 'https://'); 9 | assert(env != null); 10 | runApp(MyApp()); 11 | } 12 | 13 | class MyApp extends StatelessWidget { 14 | // This widget is the root of your application. 15 | @override 16 | Widget build(BuildContext context) { 17 | return MaterialApp( 18 | title: 'Flutter Demo', 19 | theme: ThemeData( 20 | // This is the theme of your application. 21 | // 22 | // Try running your application with "flutter run". You'll see the 23 | // application has a blue toolbar. Then, without quitting the app, try 24 | // changing the primarySwatch below to Colors.green and then invoke 25 | // "hot reload" (press "r" in the console where you ran "flutter run", 26 | // or simply save your changes to "hot reload" in a Flutter IDE). 27 | // Notice that the counter didn't reset back to zero; the application 28 | // is not restarted. 29 | primarySwatch: Colors.blue, 30 | // This makes the visual density adapt to the platform that you run 31 | // the app on. For desktop platforms, the controls will be smaller and 32 | // closer together (more dense) than on mobile platforms. 33 | visualDensity: VisualDensity.adaptivePlatformDensity, 34 | ), 35 | home: MyHomePage(title: 'Flutter Demo Home Page'), 36 | ); 37 | } 38 | } 39 | 40 | class MyHomePage extends StatefulWidget { 41 | MyHomePage({Key key, this.title}) : super(key: key); 42 | 43 | // This widget is the home page of your application. It is stateful, meaning 44 | // that it has a State object (defined below) that contains fields that affect 45 | // how it looks. 46 | 47 | // This class is the configuration for the state. It holds the values (in this 48 | // case the title) provided by the parent (in this case the App widget) and 49 | // used by the build method of the State. Fields in a Widget subclass are 50 | // always marked "final". 51 | 52 | final String title; 53 | 54 | @override 55 | _MyHomePageState createState() => _MyHomePageState(); 56 | } 57 | 58 | class _MyHomePageState extends State { 59 | int _counter = 0; 60 | 61 | void _incrementCounter() { 62 | setState(() { 63 | // This call to setState tells the Flutter framework that something has 64 | // changed in this State, which causes it to rerun the build method below 65 | // so that the display can reflect the updated values. If we changed 66 | // _counter without calling setState(), then the build method would not be 67 | // called again, and so nothing would appear to happen. 68 | _counter++; 69 | }); 70 | } 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | // This method is rerun every time setState is called, for instance as done 75 | // by the _incrementCounter method above. 76 | // 77 | // The Flutter framework has been optimized to make rerunning build methods 78 | // fast, so that you can just rebuild anything that needs updating rather 79 | // than having to individually change instances of widgets. 80 | return Scaffold( 81 | appBar: AppBar( 82 | // Here we take the value from the MyHomePage object that was created by 83 | // the App.build method, and use it to set our appbar title. 84 | title: Text(widget.title), 85 | ), 86 | body: Center( 87 | // Center is a layout widget. It takes a single child and positions it 88 | // in the middle of the parent. 89 | child: Column( 90 | // Column is also a layout widget. It takes a list of children and 91 | // arranges them vertically. By default, it sizes itself to fit its 92 | // children horizontally, and tries to be as tall as its parent. 93 | // 94 | // Invoke "debug painting" (press "p" in the console, choose the 95 | // "Toggle Debug Paint" action from the Flutter Inspector in Android 96 | // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) 97 | // to see the wireframe for each widget. 98 | // 99 | // Column has various properties to control how it sizes itself and 100 | // how it positions its children. Here we use mainAxisAlignment to 101 | // center the children vertically; the main axis here is the vertical 102 | // axis because Columns are vertical (the cross axis would be 103 | // horizontal). 104 | mainAxisAlignment: MainAxisAlignment.center, 105 | children: [ 106 | Text( 107 | 'You have pushed the button this many times:', 108 | ), 109 | Text( 110 | '$_counter', 111 | style: Theme.of(context).textTheme.headline4, 112 | ), 113 | ], 114 | ), 115 | ), 116 | floatingActionButton: FloatingActionButton( 117 | onPressed: _incrementCounter, 118 | tooltip: 'Increment', 119 | child: Icon(Icons.add), 120 | ), // This trailing comma makes auto-formatting nicer for build methods. 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Ignore devspace.yaml file to prevent image rebuilding after config changes 4 | devspace.yaml 5 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/.env/development.env: -------------------------------------------------------------------------------- 1 | API_AUTH_ENABLED = true 2 | DB_HOST= 1 3 | DB_PORT= 1 4 | DB_TYPE= 1 5 | DB_USER= 1 6 | DB_PASSWORD= 1 7 | DB_NAME= 1 8 | DB_TYPEORM_SYNC= false 9 | DB_TYPEORM_LOG=["all"] 10 | REDIS_PORT= 6379 11 | REDIS_URL= 1 12 | ELASRIC_HOST= 1 13 | ELASRIC_ID= 1 14 | ELASRIC_USER= 1 15 | ELASRIC_PASSWORD= 1 16 | JWT_SIGN_KEY= 1 17 | MAILER_SENDER= -------------------------------------------------------------------------------- /src/templates/nestjs-general/.env/production.env: -------------------------------------------------------------------------------- 1 | API_AUTH_ENABLED = true 2 | DB_HOST= 1 3 | DB_PORT= 1 4 | DB_TYPE= 1 5 | DB_USER= 1 6 | DB_PASSWORD= 1 7 | DB_NAME= 1 8 | DB_TYPEORM_SYNC= false 9 | DB_TYPEORM_LOG=["query, info, warn, error"] 10 | REDIS_PORT= 6379 11 | REDIS_URL= 1 12 | ELASRIC_HOST= 1 13 | ELASRIC_ID= 1 14 | ELASRIC_USER= 1 15 | ELASRIC_PASSWORD= 1 16 | JWT_SIGN_KEY= 1 17 | MAILER_SENDER= -------------------------------------------------------------------------------- /src/templates/nestjs-general/.env/provision.env: -------------------------------------------------------------------------------- 1 | API_AUTH_ENABLED = true 2 | DB_HOST= 1 3 | DB_PORT= 1 4 | DB_TYPE= 1 5 | DB_USER= 1 6 | DB_PASSWORD= 1 7 | DB_NAME= 1 8 | DB_TYPEORM_SYNC= false 9 | DB_TYPEORM_LOG=["all"] 10 | REDIS_PORT= 6379 11 | REDIS_URL= 1 12 | ELASRIC_HOST= 1 13 | ELASRIC_ID= 1 14 | ELASRIC_USER= 1 15 | ELASRIC_PASSWORD= 1 16 | JWT_SIGN_KEY= 1 17 | MAILER_SENDER= -------------------------------------------------------------------------------- /src/templates/nestjs-general/.env/test.env: -------------------------------------------------------------------------------- 1 | API_AUTH_ENABLED = true 2 | DB_HOST= 1 3 | DB_PORT= 1 4 | DB_TYPE= 1 5 | DB_USER= 1 6 | DB_PASSWORD= 1 7 | DB_NAME= 1 8 | DB_TYPEORM_SYNC= false 9 | DB_TYPEORM_LOG=["all"] 10 | REDIS_PORT= 6379 11 | REDIS_URL= 1 12 | ELASRIC_HOST= 1 13 | ELASRIC_ID= 1 14 | ELASRIC_USER= 1 15 | ELASRIC_PASSWORD= 1 16 | JWT_SIGN_KEY= 1 17 | MAILER_SENDER= -------------------------------------------------------------------------------- /src/templates/nestjs-general/.github/workflows/blank.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | matrix: 21 | node-version: [12.x] 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - run: yarn install 30 | - run: yarn build 31 | env: 32 | CI: true 33 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | src/coverage 36 | swagger-spec-v1.json 37 | 38 | 39 | # Ignore DevSpace cache and log folder 40 | .devspace/ 41 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/.np-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "yarn": true, 3 | "contents": "dist", 4 | "publishConfig": { 5 | "access": "public" 6 | } 7 | } -------------------------------------------------------------------------------- /src/templates/nestjs-general/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /src/templates/nestjs-general/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug Nest Framework", 11 | "args": ["${workspaceFolder}/src/main.ts"], 12 | "runtimeArgs": [ 13 | "--nolazy", 14 | "-r", 15 | "ts-node/register", 16 | "-r", 17 | "tsconfig-paths/register" 18 | ], 19 | "sourceMaps": true, 20 | "cwd": "${workspaceRoot}", 21 | "protocol": "inspector", 22 | "env": { 23 | "NODE_ENV": "development" 24 | }, 25 | "console": "integratedTerminal" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /src/templates/nestjs-general/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM yasuoyuhao/node-docker-image-with-tini:12-alpine 2 | 3 | RUN mkdir -p /api 4 | WORKDIR /api 5 | 6 | COPY package.json . 7 | COPY yarn.lock . 8 | RUN yarn install 9 | 10 | COPY . . 11 | RUN yarn build 12 | 13 | CMD ["yarn", "start:prod"] -------------------------------------------------------------------------------- /src/templates/nestjs-general/README.md: -------------------------------------------------------------------------------- 1 | # nest.js-temp 2 | 3 | 這是一個快速執行 nest.js 7 的框架,包含了 4 | 5 | * `elasticsearch` 6 | * `typeorm` 7 | * `Socket.io` 8 | * `jwt`, `passport` 9 | * `winston` 10 | * `Custom Exception filters` 11 | * `middleware log` 12 | * `api-versioning`, `swagger` 13 | * `docker`, `docker compose`, `kubernetes devspace` 14 | * `env` 15 | * `redis` 16 | * `bitbucket pipelines` 17 | * `GCP Cloud Build` 18 | * `` 19 | 20 | run `dokcer-compose up --build` 21 | 22 | ## Update TypeORM 23 | 24 | ### first 25 | 26 | `npm i -g typeorm-model-generator` 27 | 28 | ## Cloud Redis 29 | 30 | ```=sh 31 | export REDISHOST_IP=your_redis_ip 32 | kubectl create configmap redishost --from-literal=REDISHOST=${REDISHOST_IP} 33 | ``` 34 | 35 | ## Local Redis 36 | 37 | `docker run -p 6379:6379 redis:4.0.0` 38 | 39 | ## install to k8s need 40 | 41 | * NODE_ENV 42 | * REDISHOST 43 | * PORT 44 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/bitbucket-pipelines.yml: -------------------------------------------------------------------------------- 1 | # This is a sample build configuration for JavaScript. 2 | # Check our guides at https://confluence.atlassian.com/x/14UWN for more examples. 3 | # Only use spaces to indent your .yml configuration. 4 | # ----- 5 | # You can specify a custom docker image from Docker Hub as your build environment. 6 | 7 | image: node:12 8 | 9 | clone: 10 | depth: full # SonarCloud scanner needs the full history to assign issues properly 11 | 12 | definitions: 13 | services: 14 | docker: 15 | memory: 2048 16 | caches: 17 | sonar: ~/.sonar/cache # Caching SonarCloud artifacts will speed up your build 18 | steps: 19 | - step: &build-test-sonarcloud 20 | name: Build, test and analyze on SonarCloud 21 | caches: 22 | - sonar 23 | - node 24 | services: 25 | - docker 26 | script: 27 | - yarn install 28 | - yarn build 29 | - yarn test:cov 30 | - pipe: sonarsource/sonarcloud-scan:1.2.0 31 | variables: 32 | SONAR_TOKEN: ${SONAR_TOKEN} 33 | EXTRA_ARGS: '-Dsonar.sources=src -Dsonar.tests=src -Dsonar.test.inclusions="**/testing/**,**/*.spec.ts" -Dsonar.typescript.lcov.reportPaths=src/coverage/lcov.info' 34 | pipelines: 35 | default: 36 | - step: *build-test-sonarcloud 37 | 38 | branches: 39 | develop: 40 | - step: *build-test-sonarcloud 41 | - step: 42 | name: deploy with tag 43 | script: 44 | # - apt-get update 45 | # - apt-get install -y unzip git 46 | - echo "Clone all the things!" 47 | - git tag -am "Tagging Client for release ${BITBUCKET_BUILD_NUMBER}" v${BITBUCKET_BUILD_NUMBER} 48 | - git push origin v${BITBUCKET_BUILD_NUMBER} 49 | master: 50 | - step: *build-test-sonarcloud 51 | - step: 52 | name: deploy with tag 53 | script: 54 | # - apt-get update 55 | # - apt-get install -y unzip git 56 | - echo "Clone all the things!" 57 | - git tag -am "Tagging App for release ${BITBUCKET_BUILD_NUMBER}" release${BITBUCKET_BUILD_NUMBER} 58 | - git push origin release${BITBUCKET_BUILD_NUMBER} -------------------------------------------------------------------------------- /src/templates/nestjs-general/cdk-k8s/.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | *.js 3 | !jest.config.js 4 | node_modules 5 | dist/ 6 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/cdk-k8s/__snapshots__/main.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Placeholder Empty 1`] = ` 4 | Array [ 5 | Object { 6 | "apiVersion": "apps/v1", 7 | "kind": "Deployment", 8 | "metadata": Object { 9 | "name": "test-chart-deployment-c8d4e901", 10 | }, 11 | "spec": Object { 12 | "replicas": 2, 13 | "selector": Object { 14 | "matchLabels": Object { 15 | "app": "nest-temp-k8s", 16 | }, 17 | }, 18 | "template": Object { 19 | "metadata": Object { 20 | "labels": Object { 21 | "app": "nest-temp-k8s", 22 | }, 23 | }, 24 | "spec": Object { 25 | "containers": Array [ 26 | Object { 27 | "env": Array [ 28 | Object { 29 | "name": "NODE_ENV", 30 | "value": "test", 31 | }, 32 | Object { 33 | "name": "PORT", 34 | "value": "80", 35 | }, 36 | ], 37 | "image": "yasuoyuhao/nest-temp", 38 | "imagePullPolicy": "Always", 39 | "name": "nest-temp", 40 | "ports": Array [ 41 | Object { 42 | "containerPort": 80, 43 | }, 44 | ], 45 | "resources": Object { 46 | "limits": Object { 47 | "cpu": "500m", 48 | "memory": "256Mi", 49 | }, 50 | }, 51 | }, 52 | ], 53 | }, 54 | }, 55 | }, 56 | }, 57 | ] 58 | `; 59 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/cdk-k8s/cdk8s.yaml: -------------------------------------------------------------------------------- 1 | language: typescript 2 | app: node main.js 3 | imports: 4 | - k8s 5 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/cdk-k8s/help: -------------------------------------------------------------------------------- 1 | ======================================================================================================== 2 | 3 | Your cdk8s typescript project is ready! 4 | 5 | cat help Print this message 6 | 7 | Compile: 8 | npm run compile Compile typescript code to javascript (or "yarn watch") 9 | npm run watch Watch for changes and compile typescript in the background 10 | npm run build Compile + synth 11 | 12 | Synthesize: 13 | npm run synth Synthesize k8s manifests from charts to dist/ (ready for 'kubectl apply -f') 14 | 15 | Deploy: 16 | kubectl apply -f dist/*.k8s.yaml 17 | 18 | Upgrades: 19 | npm run import Import/update k8s apis (you should check-in this directory) 20 | npm run upgrade Upgrade cdk8s modules to latest version 21 | npm run upgrade:next Upgrade cdk8s modules to latest "@next" version (last commit) 22 | 23 | ======================================================================================================== 24 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/cdk-k8s/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "" 4 | ], 5 | testMatch: [ '**/*.test.ts'], 6 | "transform": { 7 | "^.+\\.tsx?$": "ts-jest" 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/cdk-k8s/main.test.ts: -------------------------------------------------------------------------------- 1 | import {MyChart} from './main'; 2 | import {Testing} from 'cdk8s'; 3 | 4 | describe('Placeholder', () => { 5 | test('Empty', () => { 6 | const app = Testing.app(); 7 | const chart = new MyChart(app, 'test-chart'); 8 | const results = Testing.synth(chart) 9 | expect(results).toMatchSnapshot(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/cdk-k8s/main.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { App, Chart, ChartProps } from 'cdk8s'; 3 | import { KubeService, IntOrString, KubeDeployment } from './imports/k8s'; 4 | 5 | const lable = { 6 | app: 'nest-temp-k8s' 7 | }; 8 | 9 | export class MyChart extends Chart { 10 | constructor(scope: Construct, id: string, props: ChartProps = {}) { 11 | super(scope, id, props); 12 | 13 | // define resources here 14 | new KubeDeployment(this, 'deployment', { 15 | spec: { 16 | replicas: 2, 17 | selector: { 18 | matchLabels: lable 19 | }, 20 | template: { 21 | metadata: { labels: lable }, 22 | spec: { 23 | containers: [ 24 | { 25 | name: 'nest-temp', 26 | image: 'yasuoyuhao/nest-temp', 27 | imagePullPolicy: 'Always', 28 | ports: [{ containerPort: 80 }], 29 | resources: { 30 | limits: { 31 | 'cpu': '500m', 32 | 'memory': '256Mi' 33 | } 34 | }, 35 | env: [ 36 | { 37 | name: 'NODE_ENV', 38 | value: 'test' 39 | }, 40 | { 41 | name: 'PORT', 42 | value: '80' 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | } 49 | } 50 | }); 51 | } 52 | } 53 | 54 | export class MyServiceChart extends Chart { 55 | constructor(scope: Construct, id: string, props: ChartProps = {}) { 56 | super(scope, id, props); 57 | 58 | // define resources here 59 | new KubeService(this, 'service', { 60 | spec: { 61 | type: 'NodePort', 62 | ports:[ 63 | { 64 | name: 'HTTP', 65 | protocol: 'TCP', 66 | port: 80, 67 | targetPort: IntOrString.fromNumber(80), 68 | nodePort: 30800, 69 | } 70 | ], 71 | selector: lable 72 | } 73 | }); 74 | } 75 | } 76 | 77 | const app = new App(); 78 | new MyChart(app, 'cdk-k8s'); 79 | new MyServiceChart(app, 'cdk-k8s-Service'); 80 | app.synth(); 81 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/cdk-k8s/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-k8s", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "types": "main.ts", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "scripts": { 9 | "import": "cdk8s import", 10 | "synth": "cdk8s synth", 11 | "compile": "tsc", 12 | "watch": "tsc -w", 13 | "test": "jest", 14 | "test:update": "jest --updateSnapshot", 15 | "build": "npm run compile && npm run test && npm run synth", 16 | "upgrade": "npm i cdk8s@latest cdk8s-cli@latest", 17 | "upgrade:next": "npm i cdk8s@next cdk8s-cli@next" 18 | }, 19 | "dependencies": { 20 | "cdk8s": "^1.0.0-beta.3", 21 | "cdk8s-plus-17": "^1.0.0-beta.3", 22 | "constructs": "^3.2.60" 23 | }, 24 | "devDependencies": { 25 | "@types/jest": "^26.0.16", 26 | "@types/node": "^14.14.10", 27 | "cdk8s-cli": "^1.0.0-beta.3", 28 | "jest": "^26.6.3", 29 | "ts-jest": "^26.4.4", 30 | "typescript": "^4.1.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/cdk-k8s/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "charset": "utf8", 5 | "declaration": true, 6 | "experimentalDecorators": true, 7 | "inlineSourceMap": true, 8 | "inlineSources": true, 9 | "lib": [ 10 | "es2016" 11 | ], 12 | "module": "CommonJS", 13 | "noEmitOnError": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitAny": true, 16 | "noImplicitReturns": true, 17 | "noImplicitThis": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "resolveJsonModule": true, 21 | "strict": true, 22 | "strictNullChecks": true, 23 | "strictPropertyInitialization": true, 24 | "stripInternal": true, 25 | "target": "ES2017" 26 | }, 27 | "include": [ 28 | "**/*.ts" 29 | ], 30 | "exclude": [ 31 | "node_modules" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | timeout: 1800s 2 | steps: 3 | - name: 'gcr.io/cloud-builders/docker' 4 | args: ['build', '-t', 'asia.gcr.io/$PROJECT_ID/nest-temp:$TAG_NAME', '.'] 5 | id: 'build-gcr' 6 | - name: 'gcr.io/cloud-builders/docker' 7 | args: ['push', 'asia.gcr.io/$PROJECT_ID/nest-temp:$TAG_NAME'] 8 | id: 'push-gcr' 9 | waitFor: 10 | - 'build-gcr' 11 | - name: 'gcr.io/cloud-builders/kubectl' 12 | args: ['set', 'image','deployment/${_DEPLOYMENTNAME}','${_CONTAINERNAME}=asia.gcr.io/$PROJECT_ID/nest-temp:$TAG_NAME', '--record'] 13 | id: 'update-gke' 14 | env: 15 | - 'CLOUDSDK_COMPUTE_ZONE=${_ZONE}' 16 | - 'CLOUDSDK_CONTAINER_CLUSTER=${_CLUSTER}' 17 | waitFor: 18 | - 'push-gcr' 19 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/devspace.yaml: -------------------------------------------------------------------------------- 1 | version: v1beta9 2 | images: 3 | app: 4 | image: yasuoyuhao/nest-temp 5 | preferSyncOverRebuild: true 6 | injectRestartHelper: false 7 | appendDockerfileInstructions: 8 | - USER root 9 | build: 10 | docker: 11 | skipPush: true 12 | deployments: 13 | - name: nest-temp 14 | helm: 15 | componentChart: true 16 | values: 17 | containers: 18 | - image: yasuoyuhao/nest-temp 19 | env: 20 | - name: NODE_ENV 21 | value: "development" 22 | - name: PORT 23 | value: "80" 24 | service: 25 | ports: 26 | - port: 80 27 | dev: 28 | ports: 29 | - imageName: app 30 | forward: 31 | - port: 30800 32 | remotePort: 80 33 | open: 34 | - url: http://localhost:30800 35 | sync: 36 | - imageName: app 37 | disableUpload: true 38 | excludePaths: 39 | - .git/ 40 | uploadExcludePaths: 41 | - devspace.yaml 42 | onUpload: 43 | restartContainer: true 44 | profiles: 45 | - name: production 46 | patches: 47 | - op: remove 48 | path: images.app.injectRestartHelper 49 | - op: remove 50 | path: images.app.appendDockerfileInstructions 51 | - name: interactive 52 | patches: 53 | - op: add 54 | path: dev.interactive 55 | value: 56 | defaultEnabled: true 57 | - op: add 58 | path: images.app.entrypoint 59 | value: 60 | - sleep 61 | - "9999999999" 62 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | api: 5 | image: yasuoyuhao/nest-temp 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | ports: 10 | - "3000:3000" 11 | networks: 12 | - docker-ts-nest-net 13 | expose: 14 | - 3000 15 | # volumes: 16 | # - .:/api 17 | # depends_on: 18 | # - redis 19 | environment: 20 | NODE_ENV: test 21 | # redis: 22 | # image: redis:alpine 23 | # networks: 24 | # - docker-ts-nest-net 25 | 26 | networks: 27 | docker-ts-nest-net: 28 | driver: bridge -------------------------------------------------------------------------------- /src/templates/nestjs-general/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "assets": [ 6 | "static/**/*" 7 | ] 8 | } 9 | } -------------------------------------------------------------------------------- /src/templates/nestjs-general/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ext": "*", 6 | "ignore": [ 7 | "src/**/*.spec.ts" 8 | ], 9 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 10 | } -------------------------------------------------------------------------------- /src/templates/nestjs-general/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= projectName %>", 3 | "version": "1.0.0", 4 | "description": "a nest.js start temp", 5 | "author": "yasuoyuhao", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/yasuoyuhao/nest.js-temp" 10 | }, 11 | "scripts": { 12 | "prebuild": "rimraf dist", 13 | "build": "nest build", 14 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 15 | "start": "nest start", 16 | "start:dev": "nest start --watch", 17 | "start:debug": "nest start --debug --watch", 18 | "start:prod": "node dist/main", 19 | "lint": "tslint -p tsconfig.json -c tslint.json", 20 | "webpack": "webpack --config webpack.config.js", 21 | "test": "jest", 22 | "test:watch": "jest --watch", 23 | "test:cov": "jest --coverage", 24 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 25 | "test:e2e": "jest --config ./test/jest-e2e.json", 26 | "update:orm": "typeorm-model-generator -h xxx -d xxx -u xxx -x xxx -e postgres -o ./src/db/xxx && rm ./src/db/xxx/ormconfig.json ./src/db/xxx/tsconfig.json" 27 | }, 28 | "dependencies": { 29 | "@elastic/elasticsearch": "^7.10.0", 30 | "@nestjs-modules/mailer": "^1.5.1", 31 | "@nestjs/common": "^7.6.5", 32 | "@nestjs/core": "^7.6.5", 33 | "@nestjs/jwt": "^7.2.0", 34 | "@nestjs/microservices": "^7.6.5", 35 | "@nestjs/passport": "^7.1.5", 36 | "@nestjs/platform-express": "^7.6.5", 37 | "@nestjs/platform-socket.io": "^7.6.5", 38 | "@nestjs/schedule": "^0.4.1", 39 | "@nestjs/swagger": "^4.7.9", 40 | "@nestjs/typeorm": "^7.1.5", 41 | "@nestjs/websockets": "^7.6.5", 42 | "@types/handlebars": "^4.1.0", 43 | "class-transformer": "^0.3.1", 44 | "class-validator": "^0.12.2", 45 | "csv-parse": "^4.14.2", 46 | "csv-stringify": "^5.6.0", 47 | "dotenv": "^8.2.0", 48 | "handlebars": "^4.7.6", 49 | "joi": "^14.3.1", 50 | "luxon": "^1.25.0", 51 | "mathjs": "^8.1.1", 52 | "mysql": "^2.18.1", 53 | "nest-router": "^1.0.9", 54 | "nest-winston": "^1.4.0", 55 | "nodemailer": "^6.4.17", 56 | "passport": "^0.4.1", 57 | "passport-jwt": "^4.0.0", 58 | "passport-local": "^1.0.0", 59 | "redis": "^3.0.2", 60 | "reflect-metadata": "^0.1.13", 61 | "rimraf": "^3.0.2", 62 | "rxjs": "^6.6.3", 63 | "swagger-ui-express": "^4.1.6", 64 | "typeorm": "^0.2.29", 65 | "uuid": "^8.3.2", 66 | "winston": "^3.3.3" 67 | }, 68 | "devDependencies": { 69 | "@nestjs/cli": "^7.5.3", 70 | "@nestjs/schematics": "^7.2.6", 71 | "@nestjs/testing": "^7.6.5", 72 | "@types/cheerio": "^0.22.23", 73 | "@types/cron": "^1.7.2", 74 | "@types/dompurify": "^2.2.0", 75 | "@types/express": "^4.17.9", 76 | "@types/jest": "^26.0.19", 77 | "@types/joi": "^14.3.4", 78 | "@types/jsdom": "^16.2.5", 79 | "@types/luxon": "^1.25.0", 80 | "@types/mathjs": "^6.0.9", 81 | "@types/multer": "^1.4.5", 82 | "@types/node": "^14.14.20", 83 | "@types/node-rsa": "^1.0.0", 84 | "@types/passport-jwt": "^3.0.3", 85 | "@types/passport-local": "^1.0.33", 86 | "@types/redis": "^2.8.28", 87 | "@types/socket.io": "^2.1.12", 88 | "@types/supertest": "^2.0.10", 89 | "@types/uuid": "^8.3.0", 90 | "@types/webpack": "^4.41.25", 91 | "jest": "^26.6.3", 92 | "prettier": "^2.2.1", 93 | "supertest": "^6.0.1", 94 | "ts-jest": "^26.4.4", 95 | "ts-loader": "^8.0.13", 96 | "ts-node": "^9.1.1", 97 | "tsconfig-paths": "^3.9.0", 98 | "tslint": "^6.1.3", 99 | "typeorm-model-generator": "^0.4.3", 100 | "typescript": "^4.1.2", 101 | "webpack": "^4.44.2", 102 | "webpack-cli": "^4.2.0", 103 | "webpack-node-externals": "^2.5.2" 104 | }, 105 | "jest": { 106 | "moduleFileExtensions": [ 107 | "js", 108 | "json", 109 | "ts" 110 | ], 111 | "rootDir": "src", 112 | "testRegex": ".spec.ts$", 113 | "transform": { 114 | "^.+\\.(t|j)s$": "ts-jest" 115 | }, 116 | "coverageDirectory": "./coverage", 117 | "testEnvironment": "node" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/api-response/api-response.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ApiResponseService } from './api-response.service'; 3 | 4 | describe('ApiResponseService', () => { 5 | let service: ApiResponseService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ApiResponseService], 10 | }).compile(); 11 | 12 | service = module.get(ApiResponseService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/api-response/api-response.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Type } from '@nestjs/common'; 2 | import { BaseContext } from '../interface/base-context.interface'; 3 | import { ApiBusinessCode, ApiDuelRewardCode } from '../enum/api-business-code'; 4 | 5 | @Injectable() 6 | export class ApiResponseService { 7 | 8 | static generateResponse( 9 | data: T, 10 | message: string = 'Success', 11 | isSuccess = true, 12 | code = 200, 13 | businessCode: ApiBusinessCode | ApiDuelRewardCode = ApiBusinessCode.normal): BaseContext { 14 | return { 15 | success: isSuccess, 16 | code, 17 | businessCode, 18 | message, 19 | content: data 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/apiv1/apiv1.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ServiceModule } from '../services/service.module'; 3 | import { UserController } from './user/user.controller'; 4 | 5 | @Module({ 6 | imports: [ 7 | ServiceModule, 8 | ], 9 | controllers: [ 10 | UserController 11 | ], 12 | }) 13 | export class Apiv1Module { } 14 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/apiv1/user/user.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserController } from './user.controller'; 3 | import { MockTestModule } from '../../../test/mock-test.module'; 4 | 5 | describe('User Controller', () => { 6 | let controller: UserController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | imports: [ 11 | MockTestModule, 12 | ], 13 | controllers: [UserController], 14 | }).compile(); 15 | 16 | controller = module.get(UserController); 17 | }); 18 | 19 | it('should be defined', () => { 20 | expect(controller).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/apiv1/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Header, Query, Post, HttpException, HttpStatus } from '@nestjs/common'; 2 | import { ApiTags, ApiForbiddenResponse, ApiCreatedResponse, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; 3 | import { ApiBusinessCode } from '../../enum/api-business-code'; 4 | import { UserService } from '../../services/user/user.service'; 5 | import { string } from 'joi'; 6 | 7 | @ApiTags('使用者') 8 | @ApiBearerAuth() 9 | @Controller('user') 10 | export class UserController { 11 | 12 | constructor( 13 | ) { } 14 | 15 | @Post('signin') 16 | @ApiOperation({ 17 | summary: '查詢個人頁資訊', 18 | description: ` 19 | Business Code 說明:\r 20 | ${ApiBusinessCode.normal}-> 正常 21 | ${ApiBusinessCode.notfind}-> 查無資料 22 | ` 23 | }) 24 | @ApiCreatedResponse({ 25 | description: 'The record has been successfully created.', 26 | type: string 27 | }) 28 | @ApiForbiddenResponse({ description: 'Forbidden.' }) 29 | @Header('content-type', 'application/json') 30 | async signin( 31 | ): Promise { 32 | return 'Ok' 33 | } 34 | 35 | @Get('error') 36 | @ApiOperation({ 37 | summary: '查詢個人頁資訊', 38 | description: ` 39 | Business Code 說明:\r 40 | ${ApiBusinessCode.normal}-> 正常 41 | ${ApiBusinessCode.notfind}-> 查無資料 42 | ` 43 | }) 44 | @ApiCreatedResponse({ 45 | description: 'The record has been successfully created.', 46 | type: string 47 | }) 48 | @ApiForbiddenResponse({ description: 'Forbidden.' }) 49 | @Header('content-type', 'application/json') 50 | async fetchAboutMePageData( 51 | ): Promise { 52 | throw new HttpException('Forbidden', HttpStatus.FORBIDDEN); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/apiv2/apiv2.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserV2Controller } from './user/user.controller'; 3 | 4 | @Module({ 5 | controllers: [], 6 | }) 7 | export class Apiv2Module {} 8 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/apiv2/user/user.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserV2Controller } from './user.controller'; 3 | import { MockTestModule } from '../../../test/mock-test.module'; 4 | 5 | describe('User Controller', () => { 6 | let controller: UserV2Controller; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers:[ 11 | MockTestModule 12 | ], 13 | controllers: [UserV2Controller], 14 | }).compile(); 15 | 16 | controller = module.get(UserV2Controller); 17 | }); 18 | 19 | it('should be defined', () => { 20 | expect(controller).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/apiv2/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; 3 | 4 | @ApiTags('user') 5 | @ApiBearerAuth() 6 | @Controller('user') 7 | export class UserV2Controller { 8 | @Get() 9 | async find(): Promise { 10 | return 'hello'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; 2 | import { ConfigModule } from './config/config.module'; 3 | import { EventsModule } from './events/events.module'; 4 | import { RedisClientModule } from './redis/redis-client/redis-client.module'; 5 | import { Apiv1Module } from './apiv1/apiv1.module'; 6 | import { ServiceModule } from './services/service.module'; 7 | import { Apiv2Module } from './apiv2/apiv2.module'; 8 | import { Routes, RouterModule } from 'nest-router'; 9 | import { HttpLoggerMiddleware } from './middleware/http-logger.middleware'; 10 | import { AuthModule } from './auth/auth.module'; 11 | import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-winston'; 12 | import { APPWinstonModule } from './winston/appwinston.module'; 13 | import { APP_FILTER } from '@nestjs/core'; 14 | import { HttpFilterFilter } from './filter/http-filter.filter'; 15 | import { ScheduleModule } from '@nestjs/schedule'; 16 | 17 | const routes: Routes = [ 18 | { 19 | path: '/v1', 20 | module: Apiv1Module, 21 | }, 22 | { 23 | path: '/v2', 24 | module: Apiv2Module, 25 | }, 26 | ]; 27 | 28 | @Module({ 29 | imports: [ 30 | ConfigModule, 31 | APPWinstonModule, 32 | ScheduleModule.forRoot(), 33 | // TypeOrmConfigModule, 34 | // RedisClientModule, 35 | EventsModule, 36 | // ElasticsearchConfigModule, 37 | AuthModule, 38 | ServiceModule, 39 | RouterModule.forRoutes(routes), 40 | Apiv1Module, 41 | Apiv2Module, 42 | WinstonModule, 43 | ], 44 | providers: [ 45 | { 46 | provide: APP_FILTER, 47 | useClass: HttpFilterFilter, 48 | }, 49 | ], 50 | }) 51 | export class AppModule implements NestModule { 52 | configure(consumer: MiddlewareConsumer) { 53 | consumer 54 | .apply(HttpLoggerMiddleware) 55 | .forRoutes('*'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PassportModule } from '@nestjs/passport'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { AuthService } from './auth.service'; 5 | import { ConfigModule } from '../config/config.module'; 6 | import { ConfigService } from '../config/config.service'; 7 | import { StrategyModule } from './domain/strategy.module'; 8 | 9 | @Module({ 10 | imports: [ 11 | PassportModule, 12 | JwtModule.registerAsync({ 13 | imports: [ConfigModule], 14 | useFactory: async (configService: ConfigService) => ({ 15 | secret: configService.jwtSignKey, 16 | signOptions: { expiresIn: '3d' }, 17 | }), 18 | inject: [ConfigService], 19 | }), 20 | StrategyModule, 21 | ], 22 | providers: [AuthService], 23 | exports: [AuthService], 24 | }) 25 | export class AuthModule {} 26 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthService } from './auth.service'; 3 | import { UserService } from '../services/user/user.service'; 4 | 5 | describe('AuthService', () => { 6 | let service: AuthService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | imports: [ 11 | ], 12 | providers: [ 13 | 14 | ] 15 | }).compile(); 16 | 17 | // service = module.get(AuthService); 18 | }); 19 | 20 | it('should be defined', () => { 21 | expect(true).toBeDefined(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { UserService } from '../services/user/user.service'; 3 | import { JwtService } from '@nestjs/jwt'; 4 | 5 | @Injectable() 6 | export class AuthService { 7 | 8 | constructor( 9 | private readonly jwtService: JwtService 10 | ) {} 11 | 12 | async login(username: string, userId: string): Promise<{access_token: string}> { 13 | const payload = { username: username, sub: userId }; 14 | return { 15 | access_token: this.jwtService.sign(payload), 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/auth/domain/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { ConfigService } from '../../config/config.service'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | import { Repository } from 'typeorm'; 7 | 8 | @Injectable() 9 | export class JwtStrategy extends PassportStrategy(Strategy) { 10 | constructor( 11 | private readonly configService: ConfigService) { 12 | super({ 13 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 14 | ignoreExpiration: false, 15 | secretOrKey: configService.jwtSignKey, 16 | }); 17 | } 18 | 19 | // 依照需求返回不同 user 模型 20 | async validate(payload: any) { 21 | return { 22 | user: 1 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/auth/domain/strategy.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { TypeOrmConfigService } from '../../type-orm-config/type-orm-config.service'; 4 | import { JwtStrategy } from './jwt.strategy'; 5 | 6 | @Module({ 7 | imports: [ 8 | TypeOrmModule.forFeature(TypeOrmConfigService.getEntities()), 9 | StrategyModule, 10 | ], 11 | providers: [JwtStrategy], 12 | exports: [JwtStrategy], 13 | }) 14 | export class StrategyModule {} 15 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/auth/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class JwtAuthGuard extends AuthGuard('jwt') {} 6 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common'; 2 | import { ConfigService } from './config.service'; 3 | import * as path from 'path'; 4 | 5 | const ENV = process.env.NODE_ENV || 'development'; 6 | @Global() 7 | @Module({ 8 | providers: [ 9 | { 10 | provide: ConfigService, 11 | useValue: new ConfigService(path.resolve('./', '.env', `${ENV}.env`)), 12 | }, 13 | ], 14 | exports: [ConfigService], 15 | }) 16 | export class ConfigModule {} 17 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as dotenv from 'dotenv'; 3 | import * as Joi from 'joi'; 4 | import * as fs from 'fs'; 5 | import { Logger } from '@nestjs/common'; 6 | import { LoggerOptions } from 'typeorm/logger/LoggerOptions'; 7 | import { GSSearchType } from '../enum/gs-search-type'; 8 | 9 | const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; 10 | 11 | export interface EnvConfig { 12 | [key: string]: string; 13 | } 14 | 15 | export class ConfigService { 16 | private readonly envConfig: EnvConfig; 17 | private readonly logger = new Logger(ConfigService.name); 18 | 19 | constructor(filePath: string) { 20 | const config = dotenv.parse(fs.readFileSync(filePath)); 21 | this.envConfig = this.validateInput(config); 22 | 23 | const tempDirPath = path.resolve(__dirname, '..', 'temp'); 24 | this.envConfig.TEMP_DIR_PATH = tempDirPath; 25 | if (!fs.existsSync(tempDirPath)) { 26 | fs.mkdirSync(tempDirPath); 27 | this.logger.log(`建立暫存目錄 ${tempDirPath}`); 28 | } 29 | } 30 | 31 | /** 32 | * Ensures all needed variables are set, and returns the validated JavaScript object 33 | * including the applied default values. 34 | */ 35 | private validateInput(envConfig: EnvConfig): EnvConfig { 36 | const envVarsSchema: Joi.ObjectSchema = Joi.object({ 37 | PORT: Joi.number().default(3000), 38 | API_AUTH_ENABLED: Joi.boolean().default(true), 39 | DB_HOST: Joi.string().default('localhost'), 40 | DB_PORT: Joi.number().default(3306), 41 | DB_TYPE: Joi.string().default('mysql'), 42 | DB_USER: Joi.string().default('localhost'), 43 | DB_PASSWORD: Joi.string().default('localhost'), 44 | DB_NAME: Joi.string().default('localhost'), 45 | DB_TYPEORM_SYNC: Joi.boolean().default(false), 46 | DB_TYPEORM_LOG: Joi.array().default(['all']), 47 | REDIS_PORT: Joi.number().default(process.env.REDISPORT || 6379), 48 | REDIS_URL: Joi.string().default(process.env.REDISHOST || 'localhost'), 49 | ELASRIC_HOST: Joi.string().default(''), 50 | ELASRIC_ID: Joi.string().default(''), 51 | ELASRIC_USER: Joi.string().default(''), 52 | ELASRIC_PASSWORD: Joi.string().default(''), 53 | JWT_SIGN_KEY: Joi.string().default(''), 54 | RABBIT_MQ_ACC: Joi.string().optional().allow('').default(''), 55 | RABBIT_MQ_PW: Joi.string().optional().allow('').default(''), 56 | RABBIT_MQ_URL: Joi.string().default(''), 57 | RABBIT_MQ_PORT: Joi.number().integer().default(5672), 58 | RABBIT_MQ_QUEUE: Joi.string().default(''), 59 | LOG_LEVEL: Joi.string().default('info'), 60 | MAILER_SENDER: Joi.string().required(), 61 | MAILER_SENDER_FROM: Joi.string().required(), 62 | MAX_FILE_SIZE: Joi.number().integer().positive().default(DEFAULT_MAX_FILE_SIZE), 63 | UPLOAD_LOCATION: Joi.string().default('uploads'), 64 | ENGINE_HOST: Joi.string().default(''), 65 | KEY_SALT: Joi.string().default(''), 66 | CRYPTO_IV: Joi.string().length(16).default(''), 67 | GOOGLE_KEY_FILE: Joi.string().default(), 68 | PROJECT_ID: Joi.string().default(), 69 | COP_WEB_HOST: Joi.string().default(), 70 | CC_CO_WEB_HOST: Joi.string().default(), 71 | }); 72 | 73 | const { error, value: validatedEnvConfig } = Joi.validate(envConfig, envVarsSchema); 74 | if (error) { 75 | this.logger.error(error.message); 76 | // throw new Error(`Config validation error: ${error.message}`); 77 | } 78 | return validatedEnvConfig; 79 | } 80 | 81 | get nodeENV(): string { 82 | return process.env.NODE_ENV; 83 | } 84 | 85 | get isDev(): boolean { 86 | return process.env.NODE_ENV === 'development' || !process.env.NODE_ENV; 87 | } 88 | 89 | get isNotProd(): boolean { 90 | return process.env.NODE_ENV !== 'production'; 91 | } 92 | 93 | get isApiAuthEnabled(): boolean { 94 | return Boolean(this.envConfig.API_AUTH_ENABLED); 95 | } 96 | 97 | get dbHost(): string { 98 | return this.envConfig.DB_HOST; 99 | } 100 | 101 | get dbPort(): number { 102 | return Number(this.envConfig.DB_PORT); 103 | } 104 | 105 | get dbType(): string { 106 | return this.envConfig.DB_TYPE; 107 | } 108 | 109 | get dbUser(): string { 110 | return this.envConfig.DB_USER; 111 | } 112 | 113 | get dbPassword(): string { 114 | return this.envConfig.DB_PASSWORD; 115 | } 116 | 117 | get dbName(): string { 118 | return this.envConfig.DB_NAME; 119 | } 120 | 121 | get logDbName(): string { 122 | return this.envConfig.DB_NAME_LOG; 123 | } 124 | 125 | get adminDbName(): string { 126 | return this.envConfig.DB_NAME_ADMIN; 127 | } 128 | 129 | get dbSYNC(): boolean { 130 | return Boolean(this.envConfig.DB_TYPEORM_SYNC); 131 | } 132 | 133 | get dbLogging(): LoggerOptions { 134 | return this.envConfig.DB_TYPEORM_LOG as LoggerOptions; 135 | } 136 | 137 | get redisPort(): number { 138 | return Number(this.envConfig.REDIS_PORT); 139 | } 140 | 141 | get redisURL(): string { 142 | return this.envConfig.REDIS_URL; 143 | } 144 | 145 | get elasricHost(): string { 146 | return this.envConfig.ELASRIC_HOST; 147 | } 148 | 149 | get elasricHostID(): string { 150 | return this.envConfig.ELASRIC_ID; 151 | } 152 | 153 | get elasricUser(): string { 154 | return this.envConfig.ELASRIC_USER; 155 | } 156 | 157 | get elasricPassword(): string { 158 | return this.envConfig.ELASRIC_PASSWORD; 159 | } 160 | 161 | get jwtSignKey(): string { 162 | return this.envConfig.JWT_SIGN_KEY; 163 | } 164 | 165 | get mailerSender(): string { 166 | return this.envConfig.MAILER_SENDER; 167 | } 168 | 169 | get mailerSenderFrom(): string { 170 | return this.envConfig.MAILER_SENDER_FROM; 171 | } 172 | 173 | get maxFileSize(): number { 174 | return Number(this.envConfig.MAX_FILE_SIZE); 175 | } 176 | 177 | get uploadLocation(): string { 178 | return path.resolve(__dirname, '..', this.envConfig.UPLOAD_LOCATION); 179 | } 180 | 181 | get rabbitMqAccount(): string { 182 | return this.envConfig.RABBIT_MQ_ACC; 183 | } 184 | 185 | get rabbitMqPassword(): string { 186 | return this.envConfig.RABBIT_MQ_PW; 187 | } 188 | 189 | get rabbitMqUrl(): string { 190 | return this.envConfig.RABBIT_MQ_URL; 191 | } 192 | 193 | get rabbitMqPort(): number { 194 | return Number(this.envConfig.RABBIT_MQ_PORT); 195 | } 196 | 197 | get rabbitMqQueue(): string { 198 | return this.envConfig.RABBIT_MQ_QUEUE; 199 | } 200 | 201 | get engineHost(): string { 202 | return this.envConfig.ENGINE_HOST; 203 | } 204 | 205 | get keySalt(): string { 206 | return this.envConfig.KEY_SALT; 207 | } 208 | 209 | get cryptoIv(): string { 210 | return this.envConfig.CRYPTO_IV; 211 | } 212 | 213 | get googleKeyFile(): string { 214 | return this.envConfig.GOOGLE_KEY_FILE; 215 | } 216 | 217 | get projectId(): string { 218 | return this.envConfig.PROJECT_ID; 219 | } 220 | 221 | get bucketImage(): string { 222 | return this.isNotProd ? GSSearchType.TcImage : GSSearchType.TcImageProd; 223 | } 224 | 225 | get bucketFile(): string { 226 | return this.isNotProd ? GSSearchType.TcFile : GSSearchType.TcFileProd; 227 | } 228 | 229 | get bucketPdf(): string { 230 | return this.isNotProd ? GSSearchType.TcPdf : GSSearchType.TcPdfProd; 231 | } 232 | 233 | get logLevel(): string { 234 | return this.envConfig.LOG_LEVEL; 235 | } 236 | 237 | get copWebHost(): string { 238 | return this.envConfig.COP_WEB_HOST; 239 | } 240 | 241 | get ccCoWebHost(): string { 242 | return this.envConfig.CC_CO_WEB_HOST; 243 | } 244 | 245 | get tempDirPath(): string { 246 | return this.envConfig.TEMP_DIR_PATH; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/decorators/files/csv-validator/csv-validator.ts: -------------------------------------------------------------------------------- 1 | import { unlinkSync, writeFileSync, readFileSync } from 'fs'; 2 | import createCsvParser = require('csv-parse'); 3 | import parseCsv = require('csv-parse/lib/sync'); 4 | import stringifyCsv = require('csv-stringify'); 5 | import { InvalidCsvFormatException } from '../../../errors/service-exceptions.interface'; 6 | import { TrackerLogger } from '../../../winston/logger-helper.service'; 7 | import { validateEmailColumn, CsvRowData, REQUIRED_COLUMN_NAMES } from './field-validator'; 8 | 9 | export const CSV_PARSER_OPTIONS: createCsvParser.Options = { 10 | columns: REQUIRED_COLUMN_NAMES, 11 | skipEmptyLines: true, 12 | trim: true, 13 | }; 14 | 15 | /** 16 | * 讀取並解析目標 CSV 檔案 17 | * @param filePath CSV 檔案位址 18 | * @param logger Logger 19 | * @returns 解析完後的 CSV 資料列 20 | */ 21 | export function readAndParseCsvFile(filePath: string, logger: TrackerLogger): CsvRowData[] { 22 | try { 23 | const fileContent = readFileSync(filePath, { flag: 'r+', encoding: 'utf8' }); 24 | const csvRows = parseCsv(fileContent, CSV_PARSER_OPTIONS) as CsvRowData[]; 25 | return csvRows; 26 | } catch (err) { 27 | logger.error(err); 28 | 29 | // 非 CSV 解析錯誤 30 | if (!(err instanceof createCsvParser.CsvError)) { 31 | logger.error('讀取 CSV 檔案時發生錯誤'); 32 | throw new InvalidCsvFormatException('讀取 CSV 檔案時發生錯誤'); 33 | } 34 | 35 | // 以下為 CSV 解析錯誤 36 | const { code, lines, column } = err; 37 | let message: string; 38 | if (code === 'CSV_RECORD_DONT_MATCH_COLUMNS_LENGTH') { 39 | message = `第 ${lines} 列上的欄位數量不為規定的 ${REQUIRED_COLUMN_NAMES.length} 個欄`; 40 | } else if (code === 'CSV_QUOTE_NOT_CLOSED') { 41 | message = `第 ${lines} 列上的欄位 ${column} 的字串引號沒有閉合`; 42 | } else { 43 | message = 'CSV 檔案內容格式錯誤'; 44 | } 45 | 46 | logger.error(message); 47 | throw new InvalidCsvFormatException(message); 48 | } 49 | } 50 | 51 | /** 52 | * 格式化 CSV 資料列陣列至字串 53 | * @param csvDataRows CSV 資料列陣列 54 | * @param logger Logger 55 | * @returns 格式化成字串後的 CSV 內容 56 | */ 57 | function formatCsv(csvDataRows: CsvRowData[], logger: TrackerLogger): Promise { 58 | logger.log('格式化 CSV 資料列陣列 ...'); 59 | return new Promise((resolve, reject) => { 60 | stringifyCsv( 61 | csvDataRows, 62 | { 63 | header: true, 64 | columns: REQUIRED_COLUMN_NAMES, 65 | }, 66 | (err, csvContent) => { 67 | if (err) { 68 | logger.error(`格式化 CSV 資料列陣列時候發生了錯誤:\n${err}`); 69 | reject(err); 70 | return; 71 | } 72 | resolve(csvContent); 73 | } 74 | ); 75 | }); 76 | } 77 | 78 | /** 79 | * 驗證 CSV 中每個資料列中的資料 80 | * @param csvRows CSV 資料列陣列 81 | * @param logger Logger 82 | * @return 格式化後的 CSV 內容(字串) 83 | */ 84 | async function validateCsvRows(csvRows: CsvRowData[], logger: TrackerLogger): Promise { 85 | logger.log('檢查資料列的總數是否大於 0 ...'); 86 | const rowLength = csvRows.length; 87 | if (rowLength <= 1) { 88 | logger.error('CSV 只有標頭欄位,沒有任何資料欄位'); 89 | throw new InvalidCsvFormatException('CSV 只有標頭欄位,沒有任何資料欄位'); 90 | } 91 | 92 | logger.log('檢查標頭列(Headers)欄位格式 ...'); 93 | const headers = csvRows[0]; 94 | REQUIRED_COLUMN_NAMES.forEach((key, index) => { 95 | if (headers[key] !== key) { 96 | throw new InvalidCsvFormatException(`標頭列(Headers)的第 ${index + 1} 欄位應為 ${key}`); 97 | } 98 | }); 99 | 100 | logger.log('檢查每個資料列 ...'); 101 | const emailSet = new Set(); 102 | for (let i = 1; i < rowLength; i++) { 103 | const rowNumber = i + 1; 104 | const csvData = csvRows[i]; 105 | 106 | logger.log(`檢查第 ${rowNumber} 列的資料 ...`); 107 | validateEmailColumn(csvData.email, rowNumber); 108 | if (emailSet.has(csvData.email)) { 109 | throw new InvalidCsvFormatException(`第 ${rowNumber} 列上的 email 欄位與前面的欄位重複`); 110 | } 111 | emailSet.add(csvData.email); 112 | } 113 | 114 | logger.log('資料檢查完成'); 115 | return formatCsv(csvRows.slice(1), logger); 116 | } 117 | 118 | /** 119 | * 更新 CSV 檔案內容 120 | * @param filePath CSV 檔案位址 121 | * @param csvContent 要寫入的 CSV 內容 122 | * @param logger Logger 123 | */ 124 | function updateFile(filePath: string, csvContent: string, logger: TrackerLogger) { 125 | console.log(csvContent); 126 | logger.log(`更新 CSV 檔案(${filePath})的內容 ...`); 127 | unlinkSync(filePath); 128 | writeFileSync(filePath, csvContent, { encoding: 'utf8' }); 129 | logger.log('更新成功'); 130 | } 131 | 132 | /** 133 | * CSV 檔案內容檢查者。 134 | */ 135 | export async function validateCsvContent(filePath: string, logger: TrackerLogger) { 136 | const csvRows = readAndParseCsvFile(filePath, logger); 137 | const formattedCsvContent = await validateCsvRows(csvRows, logger); 138 | updateFile(filePath, formattedCsvContent, logger); 139 | } 140 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/decorators/files/csv-validator/field-validator.ts: -------------------------------------------------------------------------------- 1 | import { isEmail } from 'class-validator'; 2 | import { InvalidCsvFormatException } from '../../../errors/service-exceptions.interface'; 3 | 4 | export const REQUIRED_COLUMN_NAMES = [ 5 | 'email', 6 | 'title', 7 | 'institution', 8 | 'name', 9 | 'content', 10 | 'officialId', 11 | 'date', 12 | 'other', 13 | 'other2', 14 | 'other3', 15 | ]; 16 | 17 | export interface CsvRowData { 18 | email: string; 19 | title: string; 20 | institution: string; 21 | name: string; 22 | content: string; 23 | officialId: string; 24 | date: string; 25 | other: string; 26 | other2: string; 27 | other3: string; 28 | } 29 | 30 | /** 31 | * CSV 樣板內容。 32 | */ 33 | export const SAMPLE_CSV = REQUIRED_COLUMN_NAMES.join(',') + '\n'; 34 | 35 | export type FieldValidatorFunc = (value: string, lineNumber: number) => string | void; 36 | 37 | export const validateEmailColumn: FieldValidatorFunc = (value, lineNumber) => { 38 | if (!value) { 39 | throw new InvalidCsvFormatException(`第 ${lineNumber} 列的 email 欄位中的值不能為未定義`); 40 | } 41 | 42 | const email = value.toLowerCase().trim(); 43 | if (!isEmail(email)) { 44 | throw new InvalidCsvFormatException( 45 | `第 ${lineNumber} 列的 email 欄位上必須有合法的電子信箱地址` 46 | ); 47 | } 48 | return email; 49 | }; 50 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/decorators/files/upload-csv-file.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UseInterceptors, 3 | BadRequestException, 4 | CallHandler, 5 | ExecutionContext, 6 | Inject, 7 | mixin, 8 | NestInterceptor, 9 | Optional, 10 | Type, 11 | } from '@nestjs/common'; 12 | import { MULTER_MODULE_OPTIONS } from '@nestjs/platform-express/multer/files.constants'; 13 | import { MulterModuleOptions } from '@nestjs/platform-express/multer'; 14 | import * as fs from 'fs'; 15 | import * as multer from 'multer'; 16 | import { Observable } from 'rxjs'; 17 | import { finalize } from 'rxjs/operators'; 18 | import { Multer, FileFilterCallback } from 'multer'; 19 | import { Request, Response } from 'express'; 20 | import { transformException } from '../../errors/multer-transform-exception'; 21 | import { MulterFile } from '../../interface/multer-file.interface'; 22 | import { validateCsvContent } from './csv-validator/csv-validator'; 23 | import { 24 | LoggerHelperService, 25 | TrackerLogger, 26 | TrackerLoggerCreator, 27 | } from '../../winston/logger-helper.service'; 28 | import { AppRequest } from '../../interface/user-id.interface'; 29 | 30 | /** 31 | * Csv 檔案攔截器。用於檢測使用者是否上傳正確的 csv 檔案。 32 | * @param fieldName 檔案放在 `multipart/form-data` 中的欄位名稱。 33 | * @param deleteFileAfterwards 是否在一回 Request/Response 處理完成後將檔案刪除。預設為 `true`。 34 | */ 35 | export function CsvFileInterceptor( 36 | fieldName: string, 37 | deleteFileAfterwards: boolean = true 38 | ): Type { 39 | class MixinInterceptor implements NestInterceptor { 40 | private trackerLoggerCreator: TrackerLoggerCreator; 41 | protected multer: Multer; 42 | 43 | constructor( 44 | @Optional() 45 | @Inject(MULTER_MODULE_OPTIONS) 46 | options: MulterModuleOptions = {}, 47 | loggerHelperService: LoggerHelperService 48 | ) { 49 | // 與 Global 的設定合併並複寫檔案篩檢的函式 50 | this.multer = multer({ 51 | ...options, 52 | ...{ 53 | fileFilter: this.fileFilter, 54 | }, 55 | }); 56 | this.trackerLoggerCreator = loggerHelperService.makeCreator(CsvFileInterceptor.name); 57 | } 58 | 59 | /** 60 | * 檢測上傳的檔案的副檔名是否為 csv 61 | * @param req Express 的 Request 物件 62 | * @param file Multer 的檔案物件 63 | * @param cb 結果回呼函式 64 | */ 65 | fileFilter(req: Request, file: MulterFile, cb: FileFilterCallback) { 66 | if (file.mimetype !== 'text/csv') { 67 | return cb(new BadRequestException(`${fieldName} 欄位只接受 csv 檔案類型`)); 68 | } 69 | cb(null, true); 70 | } 71 | 72 | /** 73 | * 透過 Multer 自 Request Body 中抽出檔案,並將結果放置到 Request 上 74 | * @param request Express 的 Request 物件 75 | * @param response Express 的 Response 物件 76 | */ 77 | extractFileFromRequest(request: Request, response: Response): Promise { 78 | return new Promise((resolve, reject) => { 79 | const handler = this.multer.single(fieldName); 80 | handler(request, response, (err: any) => { 81 | if (err) { 82 | const error = transformException(err); 83 | return reject(error); 84 | } 85 | resolve(); 86 | }); 87 | }); 88 | } 89 | 90 | /** 91 | * 檢查檔案是否有被上傳。 92 | * @param file Multer 的檔案物件 93 | * @throws `BadRequestException` 94 | */ 95 | checkIfFileExists(file: MulterFile | undefined) { 96 | if (!file) { 97 | throw new BadRequestException(`必須上傳一份 csv 檔案(欄位 ${fieldName} 為空)`); 98 | } 99 | } 100 | 101 | /** 102 | * 檢查檔案內容。如果檔案內容不符合規定則會刪除檔案並擲出例外。 103 | * @param file Multer 檔案物件。 104 | * @param logger Logger 物件。 105 | * @throws `InvalidCsvFormatException` 106 | */ 107 | async validateFile(file: MulterFile | undefined, logger: TrackerLogger) { 108 | try { 109 | await validateCsvContent(file.path, logger); 110 | } catch (err) { 111 | logger.log(`刪除檔案 ${file.path}`); 112 | fs.unlinkSync(file.path); 113 | throw err; 114 | } 115 | } 116 | 117 | /** 118 | * 建立完成一回 Request/Response 的後續動作。 119 | * @param file 上傳的檔案 120 | * @param logger Logger 物件。 121 | */ 122 | createCleanUp(file: MulterFile, logger: TrackerLogger) { 123 | if (!deleteFileAfterwards) return () => {}; 124 | return () => { 125 | if (fs.existsSync(file.path)) { 126 | logger.log(`刪除檔案 ${file.path}`); 127 | fs.unlinkSync(file.path); 128 | } 129 | }; 130 | } 131 | 132 | /** 133 | * 執行自 Request 中將 csv 檔案抽出、轉換並檢查的工作 134 | * @param context 前後文執行物件 135 | * @param next 呼叫對應的 Controller 上的 handler 136 | */ 137 | async intercept(context: ExecutionContext, next: CallHandler): Promise> { 138 | const ctx = context.switchToHttp(); 139 | const request = ctx.getRequest>(); 140 | const response = ctx.getResponse(); 141 | const logger = this.trackerLoggerCreator.create(request.trackingId); 142 | 143 | await this.extractFileFromRequest(request, response); 144 | this.checkIfFileExists(request.file); 145 | await this.validateFile(request.file, logger); 146 | 147 | const cleanUp = this.createCleanUp(request.file, logger); 148 | return next.handle().pipe(finalize(cleanUp)); 149 | } 150 | } 151 | 152 | const Interceptor = mixin(MixinInterceptor); 153 | return Interceptor as Type; 154 | } 155 | 156 | /** 157 | * 裝飾器。用於 Controller Method 上,擷取並檢測使用者是否上傳正確的 csv 檔案。 158 | * @param fieldName 檔案放在 `multipart/form-data` 中的欄位名稱。 159 | * @param deleteFileAfterwards 是否在一回 Request/Response 處理完成後將檔案刪除。預設為 `true`。 160 | */ 161 | export function UploadCsvFile(fieldName: string, deleteFileAfterwards: boolean = true) { 162 | return UseInterceptors(CsvFileInterceptor(fieldName, deleteFileAfterwards)); 163 | } 164 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/decorators/files/upload-image.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UseInterceptors, 3 | BadRequestException, 4 | CallHandler, 5 | ExecutionContext, 6 | Inject, 7 | mixin, 8 | NestInterceptor, 9 | Optional, 10 | Type, 11 | } from '@nestjs/common'; 12 | import { MULTER_MODULE_OPTIONS } from '@nestjs/platform-express/multer/files.constants'; 13 | import { MulterModuleOptions } from '@nestjs/platform-express/multer'; 14 | import * as fs from 'fs'; 15 | import * as multer from 'multer'; 16 | import { Observable } from 'rxjs'; 17 | import { finalize } from 'rxjs/operators'; 18 | import { Multer, FileFilterCallback } from 'multer'; 19 | import { Request, Response } from 'express'; 20 | import { transformException } from '../../errors/multer-transform-exception'; 21 | import { MulterFile } from '../../interface/multer-file.interface'; 22 | import { 23 | LoggerHelperService, 24 | TrackerLogger, 25 | TrackerLoggerCreator, 26 | } from '../../winston/logger-helper.service'; 27 | import { AppRequest } from '../../interface/user-id.interface'; 28 | 29 | /** 30 | * 圖片檔案攔截器。用於檢測使用者是否上傳正確的圖片檔案。 31 | * @param fieldName 檔案放在 `multipart/form-data` 中的欄位名稱。 32 | * @param deleteFileAfterwards 是否在一回 Request/Response 處理完成後將檔案刪除。預設為 `true`。 33 | */ 34 | export function ImageInterceptor( 35 | fieldName: string, 36 | deleteFileAfterwards: boolean = true 37 | ): Type { 38 | class MixinInterceptor implements NestInterceptor { 39 | private trackerLoggerCreator: TrackerLoggerCreator; 40 | protected multer: Multer; 41 | 42 | constructor( 43 | @Optional() 44 | @Inject(MULTER_MODULE_OPTIONS) 45 | options: MulterModuleOptions = {}, 46 | loggerHelperService: LoggerHelperService 47 | ) { 48 | // 與 Global 的設定合併並複寫檔案篩檢的函式 49 | this.multer = multer({ 50 | ...options, 51 | ...{ 52 | fileFilter: this.fileFilter, 53 | }, 54 | }); 55 | this.trackerLoggerCreator = loggerHelperService.makeCreator('Images'); 56 | } 57 | 58 | /** 59 | * 檢測上傳的檔案的副檔名是否為可接受的圖像種類 60 | * @param req Express 的 Request 物件 61 | * @param file Multer 的檔案物件 62 | * @param cb 結果回呼函式 63 | */ 64 | fileFilter(req: Request, file: MulterFile, cb: FileFilterCallback) { 65 | if (!file.mimetype.match(/^image\/(jpg|jpeg|png|gif)$/)) { 66 | return cb(new BadRequestException(`${fieldName} 欄位只接受 JPG, PNG 或 GIF 檔案類型`)); 67 | } 68 | cb(null, true); 69 | } 70 | 71 | /** 72 | * 透過 Multer 自 Request Body 中抽出檔案,並將結果放置到 Request 上 73 | * @param request Express 的 Request 物件 74 | * @param response Express 的 Response 物件 75 | */ 76 | extractFileFromRequest(request: Request, response: Response): Promise { 77 | return new Promise((resolve, reject) => { 78 | const handler = this.multer.single(fieldName); 79 | handler(request, response, (err: any) => { 80 | if (err) { 81 | const error = transformException(err); 82 | return reject(error); 83 | } 84 | resolve(); 85 | }); 86 | }); 87 | } 88 | 89 | /** 90 | * 檢查檔案是否有被上傳。 91 | * @param file Multer 的檔案物件 92 | * @throws `BadRequestException` 93 | */ 94 | checkIfFileExists(file: MulterFile | undefined) { 95 | if (!file) { 96 | throw new BadRequestException(`必須上傳一份圖像檔案(欄位 ${fieldName} 為空)`); 97 | } 98 | } 99 | 100 | /** 101 | * 建立完成一回 Request/Response 的後續動作。 102 | * @param file 上傳的檔案 103 | * @param logger Logger 物件。 104 | */ 105 | createCleanUp(file: MulterFile, logger: TrackerLogger) { 106 | if (!deleteFileAfterwards) return () => {}; 107 | return () => { 108 | if (fs.existsSync(file.path)) { 109 | logger.log(`刪除檔案 ${file.path}`); 110 | fs.unlinkSync(file.path); 111 | } 112 | }; 113 | } 114 | 115 | /** 116 | * 執行自 Request 中將圖像檔案抽出、轉換並檢查的工作 117 | * @param context 前後文執行物件 118 | * @param next 呼叫對應的 Controller 上的 handler 119 | */ 120 | async intercept(context: ExecutionContext, next: CallHandler): Promise> { 121 | const ctx = context.switchToHttp(); 122 | const request = ctx.getRequest>(); 123 | const response = ctx.getResponse(); 124 | const logger = this.trackerLoggerCreator.create(request.trackingId); 125 | await this.extractFileFromRequest(request, response); 126 | this.checkIfFileExists(request.file); 127 | 128 | const cleanUp = this.createCleanUp(request.file, logger); 129 | return next.handle().pipe(finalize(cleanUp)); 130 | } 131 | } 132 | 133 | const Interceptor = mixin(MixinInterceptor); 134 | return Interceptor as Type; 135 | } 136 | 137 | /** 138 | * 裝飾器。用於 Controller Method 上,擷取並檢測使用者是否上傳正確的圖像檔案。 139 | * @param fieldName 檔案放在 `multipart/form-data` 中的欄位名稱。 140 | * @param deleteFileAfterwards 是否在一回 Request/Response 處理完成後將檔案刪除。預設為 `true`。 141 | */ 142 | export function UploadImageFile(fieldName: string, deleteFileAfterwards: boolean = true) { 143 | return UseInterceptors(ImageInterceptor(fieldName, deleteFileAfterwards)); 144 | } 145 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/decorators/files/upload-pdf-file.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UseInterceptors, 3 | BadRequestException, 4 | CallHandler, 5 | ExecutionContext, 6 | Inject, 7 | mixin, 8 | NestInterceptor, 9 | Optional, 10 | Type, 11 | } from '@nestjs/common'; 12 | import { MULTER_MODULE_OPTIONS } from '@nestjs/platform-express/multer/files.constants'; 13 | import { MulterModuleOptions } from '@nestjs/platform-express/multer'; 14 | import * as fs from 'fs'; 15 | import * as multer from 'multer'; 16 | import { Observable } from 'rxjs'; 17 | import { finalize } from 'rxjs/operators'; 18 | import { Multer, FileFilterCallback } from 'multer'; 19 | import { Request, Response } from 'express'; 20 | import { transformException } from '../../errors/multer-transform-exception'; 21 | import { MulterFile } from '../../interface/multer-file.interface'; 22 | import { 23 | LoggerHelperService, 24 | TrackerLogger, 25 | TrackerLoggerCreator, 26 | } from '../../winston/logger-helper.service'; 27 | import { AppRequest } from '../../interface/user-id.interface'; 28 | 29 | /** 30 | * Pdf 檔案攔截器。用於檢測使用者是否上傳正確的 pdf 檔案。 31 | * @param fieldName 檔案放在 `multipart/form-data` 中的欄位名稱。 32 | * @param deleteFileAfterwards 是否在一回 Request/Response 處理完成後將檔案刪除。預設為 `true`。 33 | */ 34 | export function PdfFileInterceptor( 35 | fieldName: string, 36 | deleteFileAfterwards: boolean = true 37 | ): Type { 38 | class MixinInterceptor implements NestInterceptor { 39 | private trackerLoggerCreator: TrackerLoggerCreator; 40 | protected multer: Multer; 41 | 42 | constructor( 43 | @Optional() 44 | @Inject(MULTER_MODULE_OPTIONS) 45 | options: MulterModuleOptions = {}, 46 | loggerHelperService: LoggerHelperService 47 | ) { 48 | // 與 Global 的設定合併並複寫檔案篩檢的函式 49 | this.multer = multer({ 50 | ...options, 51 | ...{ 52 | fileFilter: this.fileFilter, 53 | }, 54 | }); 55 | this.trackerLoggerCreator = loggerHelperService.makeCreator('Files'); 56 | } 57 | 58 | /** 59 | * 檢測上傳的檔案的副檔名是否為 svg 60 | * @param req Express 的 Request 物件 61 | * @param file Multer 的檔案物件 62 | * @param cb 結果回呼函式 63 | */ 64 | fileFilter(req: Request, file: MulterFile, cb: FileFilterCallback) { 65 | if (file.mimetype !== 'application/pdf') { 66 | return cb(new BadRequestException(`${fieldName} 欄位只接受 pdf 檔案類型`)); 67 | } 68 | cb(null, true); 69 | } 70 | 71 | /** 72 | * 透過 Multer 自 Request Body 中抽出檔案,並將結果放置到 Request 上 73 | * @param request Express 的 Request 物件 74 | * @param response Express 的 Response 物件 75 | */ 76 | extractFileFromRequest(request: Request, response: Response): Promise { 77 | return new Promise((resolve, reject) => { 78 | const handler = this.multer.single(fieldName); 79 | handler(request, response, (err: any) => { 80 | if (err) { 81 | const error = transformException(err); 82 | return reject(error); 83 | } 84 | resolve(); 85 | }); 86 | }); 87 | } 88 | 89 | /** 90 | * 檢查檔案是否有被上傳。 91 | * @param file Multer 的檔案物件 92 | * @throws `BadRequestException` 93 | */ 94 | checkIfFileExists(file: MulterFile | undefined) { 95 | if (!file) { 96 | throw new BadRequestException(`必須上傳一份 pdf 檔案(欄位 ${fieldName} 為空)`); 97 | } 98 | } 99 | 100 | /** 101 | * 建立完成一回 Request/Response 的後續動作。 102 | * @param file 上傳的檔案 103 | * @param logger Logger 物件。 104 | */ 105 | createCleanUp(file: MulterFile, logger: TrackerLogger) { 106 | if (!deleteFileAfterwards) return () => {}; 107 | return () => { 108 | if (fs.existsSync(file.path)) { 109 | logger.log(`刪除檔案 ${file.path}`); 110 | fs.unlinkSync(file.path); 111 | } 112 | }; 113 | } 114 | 115 | /** 116 | * 執行自 Request 中將 pdf 檔案抽出、轉換並檢查的工作 117 | * @param context 前後文執行物件 118 | * @param next 呼叫對應的 Controller 上的 handler 119 | */ 120 | async intercept(context: ExecutionContext, next: CallHandler): Promise> { 121 | const ctx = context.switchToHttp(); 122 | const request = ctx.getRequest>(); 123 | const response = ctx.getResponse(); 124 | const logger = this.trackerLoggerCreator.create(request.trackingId); 125 | await this.extractFileFromRequest(request, response); 126 | this.checkIfFileExists(request.file); 127 | 128 | const cleanUp = this.createCleanUp(request.file, logger); 129 | return next.handle().pipe(finalize(cleanUp)); 130 | } 131 | } 132 | 133 | const Interceptor = mixin(MixinInterceptor); 134 | return Interceptor as Type; 135 | } 136 | 137 | /** 138 | * 裝飾器。用於 Controller Method 上,擷取並檢測使用者是否上傳正確的 pdf 檔案。 139 | * @param fieldName 檔案放在 `multipart/form-data` 中的欄位名稱。 140 | * @param deleteFileAfterwards 是否在一回 Request/Response 處理完成後將檔案刪除。預設為 `true`。 141 | */ 142 | export function UploadPdfFile(fieldName: string, deleteFileAfterwards: boolean = true) { 143 | return UseInterceptors(PdfFileInterceptor(fieldName, deleteFileAfterwards)); 144 | } 145 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/decorators/files/upload-svg-file.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UseInterceptors, 3 | BadRequestException, 4 | CallHandler, 5 | ExecutionContext, 6 | Inject, 7 | mixin, 8 | NestInterceptor, 9 | Optional, 10 | Type, 11 | } from '@nestjs/common'; 12 | import { MULTER_MODULE_OPTIONS } from '@nestjs/platform-express/multer/files.constants'; 13 | import { MulterModuleOptions } from '@nestjs/platform-express/multer'; 14 | import * as fs from 'fs'; 15 | import * as multer from 'multer'; 16 | import { Observable } from 'rxjs'; 17 | import { finalize, tap } from 'rxjs/operators'; 18 | import { Multer, FileFilterCallback } from 'multer'; 19 | import { Request, Response } from 'express'; 20 | import { transformException } from '../../errors/multer-transform-exception'; 21 | import { MulterFile } from '../../interface/multer-file.interface'; 22 | import { 23 | LoggerHelperService, 24 | TrackerLogger, 25 | TrackerLoggerCreator, 26 | } from '../../winston/logger-helper.service'; 27 | import { AppRequest } from '../../interface/user-id.interface'; 28 | 29 | /** 30 | * Svg 檔案攔截器。用於檢測使用者是否上傳正確的 svg 檔案。 31 | * @param fieldName 檔案放在 `multipart/form-data` 中的欄位名稱。 32 | * @param deleteFileAfterwards 是否在一回 Request/Response 處理完成後將檔案刪除。預設為 `true`。 33 | */ 34 | export function SvgFileInterceptor( 35 | fieldName: string, 36 | deleteFileAfterwards: boolean = true 37 | ): Type { 38 | class MixinInterceptor implements NestInterceptor { 39 | private trackerLoggerCreator: TrackerLoggerCreator; 40 | protected multer: Multer; 41 | 42 | constructor( 43 | @Optional() 44 | @Inject(MULTER_MODULE_OPTIONS) 45 | options: MulterModuleOptions = {}, 46 | loggerHelperService: LoggerHelperService 47 | ) { 48 | // 與 Global 的設定合併並複寫檔案篩檢的函式 49 | this.multer = multer({ 50 | ...options, 51 | ...{ 52 | fileFilter: this.fileFilter, 53 | }, 54 | }); 55 | this.trackerLoggerCreator = loggerHelperService.makeCreator('Files'); 56 | } 57 | 58 | /** 59 | * 檢測上傳的檔案的副檔名是否為 svg 60 | * @param req Express 的 Request 物件 61 | * @param file Multer 的檔案物件 62 | * @param cb 結果回呼函式 63 | */ 64 | fileFilter(req: Request, file: MulterFile, cb: FileFilterCallback) { 65 | if (file.mimetype !== 'image/svg+xml') { 66 | return cb(new BadRequestException(`${fieldName} 欄位只接受 svg 檔案類型`)); 67 | } 68 | cb(null, true); 69 | } 70 | 71 | /** 72 | * 透過 Multer 自 Request Body 中抽出檔案,並將結果放置到 Request 上 73 | * @param request Express 的 Request 物件 74 | * @param response Express 的 Response 物件 75 | */ 76 | extractFileFromRequest(request: Request, response: Response): Promise { 77 | return new Promise((resolve, reject) => { 78 | const handler = this.multer.single(fieldName); 79 | handler(request, response, (err: any) => { 80 | if (err) { 81 | const error = transformException(err); 82 | return reject(error); 83 | } 84 | resolve(); 85 | }); 86 | }); 87 | } 88 | 89 | /** 90 | * 檢查檔案是否有被上傳。 91 | * @param file Multer 的檔案物件 92 | * @throws `BadRequestException` 93 | */ 94 | checkIfFileExists(file: MulterFile | undefined) { 95 | if (!file) { 96 | throw new BadRequestException(`必須上傳一份 svg 檔案(欄位 ${fieldName} 為空)`); 97 | } 98 | } 99 | 100 | /** 101 | * 建立完成一回 Request/Response 的後續動作。 102 | * @param file 上傳的檔案 103 | * @param logger Logger 物件。 104 | */ 105 | createCleanUp(file: MulterFile, logger: TrackerLogger) { 106 | if (!deleteFileAfterwards) return () => {}; 107 | return () => { 108 | if (fs.existsSync(file.path)) { 109 | logger.log(`刪除檔案 ${file.path}`); 110 | fs.unlinkSync(file.path); 111 | } 112 | }; 113 | } 114 | 115 | /** 116 | * 執行自 Request 中將 svg 檔案抽出、轉換並檢查的工作 117 | * @param context 前後文執行物件 118 | * @param next 呼叫對應的 Controller 上的 handler 119 | */ 120 | async intercept(context: ExecutionContext, next: CallHandler): Promise> { 121 | const ctx = context.switchToHttp(); 122 | const request = ctx.getRequest>(); 123 | const response = ctx.getResponse(); 124 | const logger = this.trackerLoggerCreator.create(request.trackingId); 125 | await this.extractFileFromRequest(request, response); 126 | this.checkIfFileExists(request.file); 127 | 128 | const cleanUp = this.createCleanUp(request.file, logger); 129 | return next.handle().pipe(finalize(cleanUp)); 130 | } 131 | } 132 | 133 | const Interceptor = mixin(MixinInterceptor); 134 | return Interceptor as Type; 135 | } 136 | 137 | /** 138 | * 裝飾器。用於 Controller Method 上,擷取並檢測使用者是否上傳正確的 svg 檔案。 139 | * @param fieldName 檔案放在 `multipart/form-data` 中的欄位名稱。 140 | * @param deleteFileAfterwards 是否在一回 Request/Response 處理完成後將檔案刪除。預設為 `true`。 141 | */ 142 | export function UploadSvgFile(fieldName: string, deleteFileAfterwards: boolean = true) { 143 | return UseInterceptors(SvgFileInterceptor(fieldName, deleteFileAfterwards)); 144 | } 145 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/decorators/inject-winston-logger-service.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 3 | 4 | export function InjectWinstonLoggerService() { 5 | return Inject(WINSTON_MODULE_NEST_PROVIDER); 6 | } 7 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/decorators/tracking-id.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { AppRequest } from '../interface/user-id.interface'; 3 | 4 | export const TrackingId = createParamDecorator((data: unknown, ctx: ExecutionContext) => { 5 | const request = ctx.switchToHttp().getRequest>(); 6 | return request.trackingId; 7 | }); 8 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/elasticsearch-config/elasticsearch-config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '../config/config.module'; 3 | import { ElasticsearchConfigService } from './elasticsearch-config.service'; 4 | 5 | @Module({ 6 | imports: [ConfigModule], 7 | providers: [ElasticsearchConfigService], 8 | exports: [ElasticsearchConfigService], 9 | }) 10 | export class ElasticsearchConfigModule { } 11 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/elasticsearch-config/elasticsearch-config.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ElasticsearchConfigService } from './elasticsearch-config.service'; 3 | import { ConfigService } from '../config/config.service'; 4 | import path = require('path'); 5 | const ENV = process.env.NODE_ENV || 'development'; 6 | 7 | describe('ElasticsearchConfigService', () => { 8 | let service: ElasticsearchConfigService; 9 | 10 | beforeEach(async () => { 11 | const module: TestingModule = await Test.createTestingModule({ 12 | providers: [ 13 | { 14 | provide: ConfigService, 15 | useValue: new ConfigService(path.resolve('./', '.env', `${ENV}.env`)), 16 | }, 17 | // ElasticsearchConfigService 18 | ], 19 | }).compile(); 20 | 21 | // service = module.get(ElasticsearchConfigService); 22 | }); 23 | 24 | it('should be defined', () => { 25 | expect(true).toBeDefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/elasticsearch-config/elasticsearch-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { ConfigService } from '../config/config.service'; 3 | import * as elasticsearch from '@elastic/elasticsearch'; 4 | 5 | @Injectable() 6 | export class ElasticsearchConfigService { 7 | 8 | private readonly logger = new Logger(ElasticsearchConfigService.name); 9 | 10 | client: elasticsearch.Client; 11 | constructor(private readonly configService: ConfigService) { 12 | this.client = new elasticsearch.Client({ 13 | node: this.configService.elasricHost, 14 | cloud: { 15 | id: this.configService.elasricHostID 16 | }, 17 | auth: { 18 | username: this.configService.elasricUser, 19 | password: this.configService.elasricPassword 20 | } 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/enum/account-type.ts: -------------------------------------------------------------------------------- 1 | export enum AccountType { 2 | CopAccount = 'CopAccount', 3 | CcAccount = 'CcAccount', 4 | CoAccount = 'CoAccount', 5 | } 6 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/enum/api-business-code.ts: -------------------------------------------------------------------------------- 1 | export enum ApiBusinessCode { 2 | normal = 0, 3 | notfind = 9999, 4 | signupforlessthanaweek = 9998, 5 | notfindItemContent = 9997, 6 | } 7 | 8 | export enum ApiDuelRewardCode { 9 | duelRewardHasNotBeenReached = 8999, 10 | } -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/enum/file-extension-names.ts: -------------------------------------------------------------------------------- 1 | export enum FileExtensionName { 2 | PNG = 'png', 3 | JPG = 'jpg', 4 | GIF = 'gif', 5 | SVG = 'svg', 6 | PDF = 'pdf', 7 | CSV = 'csv', 8 | } 9 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/enum/gs-search-type.ts: -------------------------------------------------------------------------------- 1 | export enum GSSearchType { 2 | TcImage = 'tc-image', 3 | TcFile = 'tc-file', 4 | TcPdf = 'tc-pdf', 5 | TcImageProd = 'tc-image-prod', 6 | TcFileProd = 'tc-file-prod', 7 | TcPdfProd = 'tc-pdf-prod', 8 | } 9 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/errors/multer-transform-exception.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, PayloadTooLargeException } from '@nestjs/common'; 2 | import { MulterError } from 'multer'; 3 | 4 | /** 5 | * 把 Multer 的錯誤轉成相對應得 HTTP 回應 6 | * @param error Multer 錯誤 7 | */ 8 | export function transformException(error: Error | undefined) { 9 | if (!error || !(error instanceof MulterError)) { 10 | // HttpException 11 | return error; 12 | } 13 | 14 | switch (error.code) { 15 | case 'LIMIT_FILE_SIZE': 16 | return new PayloadTooLargeException(error.message); 17 | case 'LIMIT_FILE_COUNT': 18 | case 'LIMIT_FIELD_KEY': 19 | case 'LIMIT_FIELD_VALUE': 20 | case 'LIMIT_FIELD_COUNT': 21 | case 'LIMIT_UNEXPECTED_FILE': 22 | case 'LIMIT_PART_COUNT': 23 | return new BadRequestException(error.message); 24 | } 25 | 26 | return error; 27 | } 28 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/errors/service-exceptions.interface.ts: -------------------------------------------------------------------------------- 1 | import { AccountType } from '../enum/account-type'; 2 | 3 | const ACCOUNT_TYPE_ACRONYM_DICTIONARY = { 4 | [AccountType.CopAccount]: 'COP', 5 | [AccountType.CcAccount]: 'CC', 6 | [AccountType.CoAccount]: 'CO', 7 | }; 8 | 9 | /** 10 | * 服務、商業邏輯性質的錯誤都所屬於此類的例外錯誤之下。 11 | */ 12 | export class ServiceException extends Error {} 13 | 14 | /** 15 | * 表示指定的資源紀錄不存在。 16 | */ 17 | export class ResourceNotFoundException extends ServiceException { 18 | /** 19 | * @param name 資源名稱 20 | * @param id 資源 Id 21 | */ 22 | constructor(name?: string, id?: string | number) { 23 | super(); 24 | if (name && id) { 25 | this.message = `id 為 ${id} 的 ${name} 紀錄不存在`; 26 | } else if (name) { 27 | this.message = `指定的 ${name} 不存在`; 28 | } else { 29 | this.message = '指定的資源不存在'; 30 | } 31 | } 32 | } 33 | 34 | /** 35 | * 表示訪客登入帳戶失敗。 36 | */ 37 | export class SignInFailureException extends ServiceException { 38 | /** 39 | * @param accountType 訪客所嘗試登入的帳戶類型。 40 | */ 41 | constructor(accountType: AccountType) { 42 | super(); 43 | const acronym = ACCOUNT_TYPE_ACRONYM_DICTIONARY[accountType]; 44 | this.message = `訪客嘗試登入 ${acronym} 帳戶失敗`; 45 | } 46 | } 47 | 48 | /** 49 | * 帳戶的密碼已經重置過了(帳戶狀態不在密碼重置狀態上) 50 | */ 51 | export class PasswordAlreadyResettedException extends ServiceException { 52 | constructor(accountType: AccountType, id: string) { 53 | super(); 54 | const acronym = ACCOUNT_TYPE_ACRONYM_DICTIONARY[accountType]; 55 | this.message = `${acronym} 帳戶(id: ${id})已經完成登入密碼重置動作`; 56 | } 57 | } 58 | 59 | /** 60 | * 資源屬性衝突例外。如:嘗試建立具有相同電子信箱的帳號。 61 | */ 62 | export class ResourceConflictException extends ServiceException { 63 | constructor(resourceName: string, fieldName: string, value: string) { 64 | super(); 65 | this.message = `資源衝撞:已有 ${resourceName} 資源在 ${fieldName} 欄位上具有值 ${value}`; 66 | } 67 | } 68 | 69 | /** 70 | * 嘗試取得不屬於該使用者的資源。 71 | */ 72 | export class OwnershipException extends ServiceException { 73 | constructor(resourceName: string, resourceId: string | number) { 74 | super(); 75 | this.message = `使用者因不擁有或沒有被授權而無法存取 ID 為 ${resourceId} 的 ${resourceName} 資源`; 76 | } 77 | } 78 | 79 | /** 80 | * 發證模板的內容錯誤。 81 | */ 82 | export class InvalidTemplateException extends ServiceException { 83 | constructor(reason: string) { 84 | super(); 85 | this.message = reason; 86 | } 87 | } 88 | 89 | /** 90 | * 指定的檔案的種類不符合需求。 91 | */ 92 | export class IncorrectFileTypeException extends ServiceException { 93 | constructor(id: string, type: string) { 94 | super(); 95 | this.message = `目標檔案(id: ${id})的種類不為 ${type}`; 96 | } 97 | } 98 | 99 | /** 100 | * 錯誤的 CSV 內容格式。 101 | */ 102 | export class InvalidCsvFormatException extends ServiceException { 103 | constructor(reason?: string) { 104 | super(); 105 | if (typeof reason === 'string') { 106 | this.message = `CSV: ${reason}`; 107 | return; 108 | } 109 | this.message = '目標 SVG 檔案的內容格式不符合規定'; 110 | } 111 | } 112 | 113 | /** 114 | * 擁有的特定資源數量已達或超出上限。 115 | */ 116 | export class ReachingMaximumResourcesException extends ServiceException { 117 | constructor(resourceName: string, count: number, isExceeded: boolean = false) { 118 | super(); 119 | const extent = isExceeded ? '超出' : '已達'; 120 | this.message = `所擁有的${resourceName}數量${extent}上限 ${count}`; 121 | } 122 | } 123 | 124 | /** 125 | * 使用者輸入的發證密碼與資料庫中儲存的不相符。 126 | */ 127 | export class IncorrectCertPasswordException extends ServiceException { 128 | constructor() { 129 | super(); 130 | this.message = '輸入的發證密碼不正確'; 131 | } 132 | } 133 | 134 | /** 135 | * 區塊鏈地址生成錯誤。 136 | */ 137 | export class BlockchainAddressGenerationException extends ServiceException { 138 | constructor() { 139 | super(); 140 | this.message = '生成區塊鏈地址時發生錯誤'; 141 | } 142 | } 143 | 144 | /** 145 | * 目標的帳戶由另一個帳戶管理。 146 | */ 147 | export class AccountManagedByAnotherException extends ServiceException { 148 | constructor(objType: string, objId: string | number, subType: string) { 149 | super(); 150 | this.message = `無權限管理 ${objType} 帳戶(id: ${objId}),因為該帳戶歸屬於另一位 ${subType} 帳戶管理`; 151 | } 152 | } 153 | 154 | /** 155 | * 新密碼與舊密碼一致。 156 | */ 157 | export class IdenticalNewOldPasswordException extends ServiceException { 158 | constructor() { 159 | super(); 160 | this.message = '新密碼與舊密碼一致,請更換成不同的密碼'; 161 | } 162 | } 163 | 164 | /** 165 | * 目標群組的狀態為「發證中」,為了保持資料一致性,因此若有影響此性質的因素則可拋出此例外。 166 | */ 167 | export class CertRecordGroupNotFinishedException extends ServiceException { 168 | constructor(id: number | string) { 169 | super(); 170 | const typeOfId = typeof id; 171 | if (typeOfId === 'number' || typeOfId === 'string') { 172 | this.message = `指定的證書群組(id: ${id})狀態仍在「發證中」,因此無法進行此操作。`; 173 | return; 174 | } 175 | this.message = '指定的證書群組狀態仍在「發證中」,因此無法進行此操作。'; 176 | } 177 | } 178 | 179 | /** 180 | * 無法撤銷指定的證書,因為該證書的狀態不為「啟用」。 181 | */ 182 | export class CannotRevokeCertException extends ServiceException { 183 | constructor(id: number) { 184 | super(); 185 | this.message = `無法撤銷指定的證書(id: ${id}),因為該證書的狀態不為「啟用」。`; 186 | } 187 | } 188 | 189 | /** 190 | * 使用者嘗試讀取一個不公開的證書且又不為證書擁有者。 191 | */ 192 | export class PrivateCertificateException extends ServiceException { 193 | constructor(id: number | string) { 194 | super(); 195 | this.message = `指定的證書紀錄(id: ${id})並不公開且使用者不為證書擁有者,因此無法存取此資料`; 196 | } 197 | } 198 | 199 | /** 200 | * 使用者提供的權杖經檢驗後失敗。 201 | */ 202 | export class TokenVerificationFailureException extends ServiceException { 203 | constructor() { 204 | super(); 205 | this.message = '權杖驗證失敗'; 206 | } 207 | } 208 | 209 | /** 210 | * 取得權杖的管道不對。 211 | * 如原本是要由電話驗證管道所取得的權杖卻變成了電子郵件管道驗證的權杖。 212 | */ 213 | export class IncorrectVerificationMethodException extends ServiceException { 214 | constructor(methodName: string) { 215 | super(); 216 | this.message = `取得權杖的管道不為 ${methodName}`; 217 | } 218 | } 219 | 220 | /** 221 | * 目標為更新指定資料,但使用者卻提供相同的資料。 222 | */ 223 | export class IdenticalValueException extends ServiceException { 224 | constructor(name: string) { 225 | super(); 226 | this.message = `提供的 ${name} 與資料庫中的一致,請換新的 ${name} 後再操作一次`; 227 | } 228 | } 229 | 230 | /** 231 | * 電子信箱驗證碼驗證失敗。 232 | */ 233 | export class InvalidEmailVerificationException extends ServiceException { 234 | constructor(reason?: string) { 235 | super(); 236 | if (typeof reason === 'string') { 237 | this.message = reason; 238 | return; 239 | } 240 | this.message = '電子信箱驗證碼驗證失敗'; 241 | } 242 | } 243 | 244 | /** 245 | * 目標證書群組的狀態為「已取消」,無法進行加發證書或是再取消的動作。 246 | */ 247 | export class CancelledCertRecordGroupException extends ServiceException { 248 | constructor(id: number) { 249 | super(); 250 | this.message = `目標證書群組(id: ${id})的狀態為「已取消」,無法進行加發證書或是再取消的動作。`; 251 | } 252 | } 253 | 254 | /** 255 | * 不合法的 PDF 檔案內容格式。 256 | */ 257 | export class InvalidBackgroundPdfFormatException extends ServiceException { 258 | constructor(reason: string) { 259 | super(reason); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/events/events.gateway.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { EventsGateway } from './events.gateway'; 3 | import { AuthService } from '../auth/auth.service'; 4 | import { ConfigModule } from '../config/config.module'; 5 | import { ConfigService } from '../config/config.service'; 6 | import { UserService } from '../services/user/user.service'; 7 | import { ServiceModule } from '../services/service.module'; 8 | import * as path from 'path'; 9 | import { MockTestServiceModule } from '../../test/mock-test-service.module'; 10 | 11 | describe('EventsGateway', () => { 12 | let gateway: EventsGateway; 13 | 14 | beforeEach(async () => { 15 | const module: TestingModule = await Test.createTestingModule({ 16 | providers: [ 17 | { 18 | provide: ConfigService, 19 | useValue: new ConfigService(path.resolve('./', '.env', `development.env`)), 20 | }, 21 | MockTestServiceModule, 22 | EventsGateway, 23 | ], 24 | }).compile(); 25 | 26 | gateway = module.get(EventsGateway); 27 | }); 28 | 29 | it('should be defined', () => { 30 | expect(gateway).toBeDefined(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/events/events.gateway.ts: -------------------------------------------------------------------------------- 1 | import { SubscribeMessage, WebSocketGateway, WebSocketServer, MessageBody, WsResponse, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets'; 2 | import { Server, Socket } from 'socket.io'; 3 | import { Observable, from } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | import { Logger, UseGuards } from '@nestjs/common'; 6 | 7 | 8 | @WebSocketGateway({ 9 | path: '/online/gateway', 10 | origins: '*:*', 11 | }) 12 | export class EventsGateway implements OnGatewayInit, 13 | OnGatewayConnection, 14 | OnGatewayDisconnect { 15 | 16 | private readonly onlineRoom = 'oRoom'; 17 | 18 | @WebSocketServer() 19 | server: Server; 20 | 21 | private readonly logger = new Logger(EventsGateway.name); 22 | 23 | handleConnection(client: Socket, ...args: any[]) { 24 | client.join(this.onlineRoom, (error) => { 25 | if (error) { 26 | this.logger.error(`join this.onlineRoom ${error}`); 27 | } 28 | }); 29 | 30 | client.broadcast.to(this.onlineRoom).emit('sys', `${client.id} join online room.`); 31 | 32 | this.logger.log(`Handle Connection ${client.id}`); 33 | 34 | this.onConnentionOrDis(); 35 | } 36 | 37 | private onConnentionOrDis() { 38 | this.server.of('/').in(this.onlineRoom).clients((error, clients: string[]) => { 39 | this.logger.log(`Now User Count in room ${this.onlineRoom}: ${clients.length}`); 40 | }); 41 | } 42 | 43 | handleDisconnect(client: Socket) { 44 | this.logger.log(`Handle Disconnect ${client.id}`); 45 | client.leaveAll(); 46 | client.broadcast.to(this.onlineRoom).emit('sys', `${client.id} leave online room.`); 47 | 48 | this.onConnentionOrDis(); 49 | } 50 | 51 | afterInit(server: Server) { 52 | this.logger.log(`After Init ${server.path()}`); 53 | } 54 | 55 | // @UseGuards(AuthGuard) 56 | @SubscribeMessage('auth') 57 | async handleMessage(client: Socket): Promise> { 58 | const event = 'events'; 59 | 60 | client.broadcast.to(this.onlineRoom).emit(event, 1); 61 | 62 | return await Observable.create((observer: { next: (arg0: WsResponse) => void; }) => { 63 | const res: WsResponse = { 64 | event, 65 | data: 1, 66 | }; 67 | observer.next(res); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/events/events.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EventsGateway } from './events.gateway'; 3 | import { AuthService } from '../auth/auth.service'; 4 | import { UserService } from '../services/user/user.service'; 5 | import { ConfigModule } from '../config/config.module'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { TypeOrmConfigService } from '../type-orm-config/type-orm-config.service'; 8 | import { AuthModule } from '../auth/auth.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | ConfigModule, 13 | AuthModule, 14 | TypeOrmModule.forFeature(TypeOrmConfigService.getEntities()), 15 | ], 16 | providers: [ 17 | UserService, 18 | EventsGateway 19 | ], 20 | }) 21 | export class EventsModule { } 22 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/filter/http-filter.filter.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpFilterFilter } from './http-filter.filter'; 2 | 3 | describe('HttpFilterFilter', () => { 4 | it('should be defined', () => { 5 | expect(new HttpFilterFilter()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/filter/http-filter.filter.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common'; 2 | import { Request, Response } from 'express'; 3 | import { ApiResponseService } from '../api-response/api-response.service'; 4 | 5 | @Catch() 6 | export class HttpFilterFilter implements ExceptionFilter { 7 | catch(exception: HttpException, host: ArgumentsHost) { 8 | 9 | const ctx = host.switchToHttp(); 10 | const response = ctx.getResponse(); 11 | const request = ctx.getRequest(); 12 | 13 | try { 14 | const status = exception.getStatus(); 15 | 16 | response 17 | .status(status) 18 | .json( 19 | ApiResponseService.generateResponse(exception.getResponse(), exception.message, false, status, 0) 20 | ); 21 | } catch(err) { 22 | response 23 | .status(HttpStatus.INTERNAL_SERVER_ERROR) 24 | .json( 25 | ApiResponseService.generateResponse(err, exception.message, false, HttpStatus.INTERNAL_SERVER_ERROR, 0) 26 | ); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/interface/base-context.interface.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class BaseContext { 4 | @ApiProperty({ description: 'API 是否成功' }) 5 | success: boolean; 6 | @ApiProperty({ description: 'API 狀態代號' }) 7 | code: number; 8 | @ApiProperty({ description: 'API 商業邏輯狀態代號' }) 9 | businessCode: number; 10 | @ApiProperty({ description: 'API 訊息' }) 11 | message: string; 12 | @ApiProperty({ description: 'API 內容' }) 13 | content: T; 14 | } 15 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/interface/multer-file.interface.ts: -------------------------------------------------------------------------------- 1 | import 'multer'; 2 | import { Express } from 'express'; 3 | 4 | export type MulterFile = Express.Multer.File; 5 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/interface/user-id.interface.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { Response } from 'express'; 3 | 4 | export interface AppRequest extends Request { 5 | user: T 6 | trackingId: string; 7 | } 8 | 9 | 10 | export interface AppResponse extends Response { 11 | user: T 12 | } 13 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/interface/user/query/user-query.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | 3 | export class UserDeviceData { 4 | @ApiProperty({ description: 'IP' }) 5 | ip: string; 6 | @ApiProperty({ description: 'iOS / Android' }) 7 | os: string; 8 | @ApiProperty({ description: '獲取系统版本' }) 9 | osVersion: string; 10 | @ApiProperty({ description: '獲取設備型號' }) 11 | device: string; 12 | @ApiProperty({ description: '獲取 App 的版本' }) 13 | appVersion: string; 14 | } 15 | 16 | export class UserDeviceQueryData { 17 | @ApiProperty({ description: 'openId' }) 18 | openId: string; 19 | @ApiProperty({ description: 'UserDeviceData' }) 20 | userDeviceData: UserDeviceData; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/interface/user/query/user-signin-data-query.interface copy.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNotEmpty } from 'class-validator'; 3 | 4 | export class UserSigninDataQuery { 5 | 6 | @ApiProperty({ description: '使用者 OpenId 的辨識號' }) 7 | @IsNotEmpty() 8 | openID: string; 9 | } -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/interface/user/response/user-signin.interface.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { BaseContext } from '../../base-context.interface'; 3 | 4 | export class UserData { 5 | 6 | // @ApiProperty({ description: '使用者基本資料' }) 7 | // roledata: RsRoledata; 8 | 9 | @ApiProperty({ description: '登入憑證' }) 10 | token: string; 11 | } 12 | 13 | // tslint:disable-next-line:max-classes-per-file 14 | export class BaseContextUserData extends BaseContext { 15 | @ApiProperty({ type: UserData }) 16 | content: UserData; 17 | } -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory, NestApplication } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 4 | import { Apiv1Module } from './apiv1/apiv1.module'; 5 | import { Apiv2Module } from './apiv2/apiv2.module'; 6 | import * as fs from 'fs'; 7 | import path = require('path'); 8 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 9 | 10 | const ENV = process.env.NODE_ENV || 'development'; 11 | console.log(`NODE_ENV: ${ENV}`); 12 | 13 | declare const module: any; 14 | 15 | async function bootstrap() { 16 | const app = await NestFactory.create(AppModule); 17 | const options = new DocumentBuilder() 18 | .setTitle('xxxx API Services') 19 | .setDescription('xxxx API Services') 20 | .setVersion('1.0') 21 | .addBearerAuth() 22 | .addServer('http://localhost:3000', 'Develop Local') 23 | .addServer('https://xxxx.klearthink.com', 'Test Env') 24 | .build(); 25 | const document = SwaggerModule.createDocument(app, options, { 26 | include: [Apiv1Module], 27 | }); 28 | 29 | const optionsV2 = new DocumentBuilder() 30 | .setTitle('xxxx API Services') 31 | .setDescription('xxxx API Services') 32 | .setVersion('2.0') 33 | .addBearerAuth() 34 | .build(); 35 | const documentV2 = SwaggerModule.createDocument(app, optionsV2, { 36 | include: [Apiv2Module], 37 | }); 38 | 39 | // 正式環境下不提供 API 文件 40 | if (process.env.NODE_ENV !== 'production') { 41 | const apiv1 = path.resolve(__dirname, 'static/api-json', 'swagger-spec-v1.json'); 42 | 43 | const dirName = path.resolve(__dirname, 'static'); 44 | 45 | if (!fs.existsSync(dirName)) { 46 | fs.mkdirSync(dirName); 47 | } 48 | 49 | const dirNameApiJson = path.resolve(__dirname, 'static/api-json'); 50 | if (!fs.existsSync(dirNameApiJson)) { 51 | fs.mkdirSync(dirNameApiJson); 52 | } 53 | 54 | if (!fs.existsSync(apiv1)) { 55 | 56 | fs.writeFileSync(apiv1, JSON.stringify(document)); 57 | } 58 | 59 | SwaggerModule.setup('apidoc/v1', app, document); 60 | 61 | const apiv2 = path.resolve(__dirname, 'static/api-json', 'swagger-spec-v2.json'); 62 | if (!fs.existsSync(apiv2)) { 63 | fs.writeFileSync(apiv2, JSON.stringify(documentV2)); 64 | } 65 | 66 | SwaggerModule.setup('apidoc/v2', app, documentV2); 67 | 68 | app.useStaticAssets(path.join(path.resolve(__dirname, 'static/api-json'))); 69 | } 70 | 71 | const port = process.env.PORT || 3000; 72 | await app.listen(port).then(() => { 73 | console.log('MicroService is starting.'); 74 | }).catch((error) => { 75 | console.error('Something wrong happened,', error); 76 | }); 77 | 78 | if (module.hot) { 79 | module.hot.accept(); 80 | module.hot.dispose(() => app.close()); 81 | } 82 | } 83 | bootstrap(); 84 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/middleware/http-logger.middleware.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpLoggerMiddleware } from './http-logger.middleware'; 2 | import { Logger } from '@nestjs/common'; 3 | 4 | describe('HttpLoggerMiddleware', () => { 5 | it('should be defined', () => { 6 | expect(new HttpLoggerMiddleware(new Logger())).toBeDefined(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/middleware/http-logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware, Logger, Inject, LoggerService } from '@nestjs/common'; 2 | import { AppRequest, AppResponse } from '../interface/user-id.interface'; 3 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 4 | import { v4 } from 'uuid'; 5 | 6 | @Injectable() 7 | export class HttpLoggerMiddleware implements NestMiddleware { 8 | 9 | private readonly logger = new Logger(HttpLoggerMiddleware.name); 10 | 11 | constructor(@Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly loggerWinston: LoggerService) { } 12 | 13 | use(req: AppRequest, res: AppResponse, next: () => void) { 14 | 15 | try { 16 | 17 | const message = { 18 | ip: req.ip, 19 | protocol: req.protocol, 20 | method: req.method, 21 | params: req.params, 22 | query: req.query, 23 | httpVersion: req.httpVersion, 24 | path: req.path, 25 | baseUrl: req.baseUrl, 26 | headers: req.headers, 27 | body: req.body 28 | } 29 | this.loggerWinston.log(message, 'HttpRequest'); 30 | 31 | // 寫入全鏈路追蹤代號 32 | const trackingId = v4(); 33 | req.trackingId = trackingId; 34 | res.setHeader('X-KT-TrackingId', trackingId); 35 | } catch(err) { 36 | this.logger.error(err) 37 | } 38 | 39 | next(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/multer/multer-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, LoggerService, BadRequestException } from '@nestjs/common'; 2 | import { MulterOptionsFactory, MulterModuleOptions } from '@nestjs/platform-express'; 3 | import * as path from 'path'; 4 | import * as fs from 'fs'; 5 | import { diskStorage } from 'multer'; 6 | import { v4 as uuidV4 } from 'uuid'; 7 | import { ConfigService } from '../config/config.service'; 8 | import { InjectWinstonLoggerService } from '../decorators/inject-winston-logger-service.decorator'; 9 | import { AppRequest } from '../interface/user-id.interface'; 10 | import { MulterFile } from '../interface/multer-file.interface'; 11 | import { FileExtensionName } from '../enum/file-extension-names'; 12 | 13 | type FileFilterCallback = (error: Error, acceptFile: boolean) => void; 14 | 15 | const MIMETYPE_TO_EXTENSION_MAP = new Map([ 16 | ['image/png', FileExtensionName.PNG], 17 | ['image/jpg', FileExtensionName.JPG], 18 | ['image/jpeg', FileExtensionName.JPG], 19 | ['image/gif', FileExtensionName.GIF], 20 | ['image/svg+xml', FileExtensionName.SVG], 21 | ['application/pdf', FileExtensionName.PDF], 22 | ['text/csv', FileExtensionName.CSV], 23 | ]); 24 | 25 | function generateFilename(mimetype: string): string { 26 | const mainName = uuidV4(); 27 | const extensionName = MIMETYPE_TO_EXTENSION_MAP.get(mimetype); 28 | if (!extensionName) { 29 | return mainName; 30 | } 31 | return `${mainName}.${extensionName}`; 32 | } 33 | 34 | @Injectable() 35 | export class MulterConfigService implements MulterOptionsFactory { 36 | private readonly uploadPath: string; 37 | 38 | constructor( 39 | private readonly configService: ConfigService, 40 | @InjectWinstonLoggerService() 41 | private readonly loggerService: LoggerService 42 | ) { 43 | this.uploadPath = configService.uploadLocation; 44 | if (!fs.existsSync(this.uploadPath)) { 45 | fs.mkdirSync(this.uploadPath); 46 | } 47 | } 48 | 49 | createMulterOptions(): MulterModuleOptions { 50 | this.loggerService.log('Creating Multer Options'); 51 | 52 | return { 53 | limits: { 54 | fileSize: +this.configService.maxFileSize || 20 * 1000 * 1000, 55 | }, 56 | fileFilter: (req: AppRequest, file: MulterFile, cb: FileFilterCallback) => { 57 | this.loggerService.log('fileFilter Options'); 58 | 59 | if (file.mimetype.match(/\/(jpg|jpeg|png|gif)$/)) { 60 | cb(null, true); 61 | } else { 62 | cb( 63 | new BadRequestException(`Unsupported file type ${path.extname(file.originalname)}`), 64 | false 65 | ); 66 | } 67 | }, 68 | storage: diskStorage({ 69 | destination: this.uploadPath, 70 | filename: (req, file, cb) => { 71 | this.loggerService.log('filename Options'); 72 | cb(null, generateFilename(file.mimetype)); 73 | }, 74 | }), 75 | }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/multer/multer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common'; 2 | import { MulterModule } from '@nestjs/platform-express'; 3 | import { MulterConfigService } from './multer-config.service'; 4 | import { ConfigModule } from '../config/config.module'; 5 | import { ConfigService } from '../config/config.service'; 6 | 7 | @Global() 8 | @Module({ 9 | imports: [ 10 | MulterModule.registerAsync({ 11 | imports: [ConfigModule], 12 | inject: [ConfigService], 13 | useClass: MulterConfigService, 14 | }), 15 | ], 16 | exports: [MulterModule], 17 | }) 18 | export class MulterAppModule {} 19 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/pipe/date-time.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, PipeTransform, Type } from '@nestjs/common'; 2 | import { DateTime } from 'luxon'; 3 | 4 | export interface ISOFormat { 5 | type: 'iso'; 6 | } 7 | 8 | export interface CustomFormat { 9 | type: 'custom'; 10 | format: string; 11 | } 12 | 13 | export type DateTimePipeFormatter = ISOFormat | CustomFormat; 14 | export type DateTimePipeOutput = 'original' | 'DateTime'; 15 | 16 | export interface DateTimePipeOptions { 17 | formatter?: DateTimePipeFormatter; 18 | output?: DateTimePipeOutput; 19 | optional?: boolean; 20 | } 21 | 22 | export class DateTimePipe implements PipeTransform { 23 | private readonly formatter: DateTimePipeFormatter; 24 | private readonly output: DateTimePipeOutput; 25 | private readonly optional: boolean; 26 | 27 | constructor(options: DateTimePipeOptions = {}) { 28 | this.formatter = options.formatter ?? { type: 'iso' }; 29 | this.output = options.output ?? 'original'; 30 | this.optional = options.optional ?? false; 31 | } 32 | 33 | transform(value: any, metadata: ArgumentMetadata) { 34 | if (typeof value === 'undefined' && this.optional) return value; 35 | 36 | const { data, type } = metadata; 37 | const name = data ?? `a ${type}`; 38 | 39 | if (typeof value !== 'string') { 40 | throw new BadRequestException(`${name} must be string`); 41 | } 42 | 43 | let dateTime: DateTime; 44 | if (this.formatter.type === 'iso') { 45 | dateTime = DateTime.fromISO(value); 46 | } else if (this.formatter.type === 'custom') { 47 | dateTime = DateTime.fromFormat(value, this.formatter.format, { zone: 'utc' }); 48 | } else { 49 | throw new Error('Unknown formatter type'); 50 | } 51 | 52 | if (!dateTime.isValid) { 53 | throw new BadRequestException(`${name} is in invalid date time format`); 54 | } 55 | 56 | return this.output === 'original' ? value : dateTime; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/pipe/general-validation.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { GeneralValidationPipe } from './general-validation.pipe'; 2 | 3 | describe('ImageLikeValidationPipe', () => { 4 | it('should be defined', () => { 5 | expect(new GeneralValidationPipe()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/pipe/general-validation.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, Injectable, PipeTransform, BadRequestException } from '@nestjs/common'; 2 | import { validate } from 'class-validator'; 3 | import { plainToClass } from 'class-transformer'; 4 | 5 | @Injectable() 6 | export class GeneralValidationPipe implements PipeTransform { 7 | async transform(value: any, { metatype }: ArgumentMetadata) { 8 | if (!metatype || !this.toValidate(metatype)) { 9 | return value; 10 | } 11 | const object = plainToClass(metatype, value); 12 | const errors = await validate(object); 13 | if (errors.length > 0) { 14 | throw new BadRequestException('Validation failed'); 15 | } 16 | return value; 17 | } 18 | 19 | private toValidate(metatype: Function): boolean { 20 | const types: Function[] = [String, Boolean, Number, Array, Object]; 21 | return !types.includes(metatype); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/pipe/integer.pipe.ts: -------------------------------------------------------------------------------- 1 | import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common'; 2 | 3 | export class IntegerPipeOptions { 4 | minimum?: number; 5 | maximum?: number; 6 | optional?: boolean; 7 | default?: number; 8 | } 9 | 10 | export class IntegerPipe implements PipeTransform { 11 | private readonly minimum?: number; 12 | private readonly maximum?: number; 13 | private readonly optional?: boolean; 14 | private readonly default?: number; 15 | 16 | constructor(options: IntegerPipeOptions = {}) { 17 | this.maximum = this.checkIntegerOptionValue('maximum', options.maximum); 18 | this.minimum = this.checkIntegerOptionValue('minimum', options.minimum); 19 | this.optional = options.optional ?? false; 20 | this.default = this.checkIntegerOptionValue('default', options.maximum); 21 | 22 | this.checkIsGreaterOrEqualTo(this.maximum, 'maximum', this.minimum, 'minimum'); 23 | this.checkIsGreaterOrEqualTo(this.default, 'default', this.minimum, 'minimum'); 24 | this.checkIsGreaterOrEqualTo(this.maximum, 'maximum', this.default, 'default'); 25 | } 26 | 27 | private checkIntegerOptionValue(optionName: string, value: number | undefined) { 28 | const type = typeof value; 29 | if (type === 'undefined') return; 30 | if (type !== 'number') throw new Error(`${optionName} should be a number or undefined`); 31 | if (!this.validateIsInteger(value)) throw new Error(`${optionName} should be an integer`); 32 | return value; 33 | } 34 | 35 | private checkIsGreaterOrEqualTo( 36 | valueA: number | undefined, 37 | nameA: string, 38 | valueB: number | undefined, 39 | nameB: string 40 | ) { 41 | if (typeof valueA === 'undefined' || typeof valueB === 'undefined') return; 42 | if (valueA < valueB) throw new Error(`${nameA} should be greater than or equal to ${nameB}`); 43 | } 44 | 45 | private validateIsInteger(value: number): boolean { 46 | return !isNaN(value) && Number.isInteger(value); 47 | } 48 | 49 | transform(value: any, metadata: ArgumentMetadata) { 50 | if (typeof value === 'undefined' && this.optional) return this.default ?? value; 51 | 52 | const { data, type } = metadata; 53 | const name = data ?? `a ${type}`; 54 | 55 | const result = Number(value); 56 | if (!this.validateIsInteger(result)) { 57 | throw new BadRequestException(`${name} should be an integer`); 58 | } 59 | 60 | if (typeof this.maximum === 'number' && result > this.maximum) { 61 | throw new BadRequestException(`${name} should be less than or equal to ${this.maximum}`); 62 | } 63 | 64 | if (typeof this.minimum === 'number' && result < this.minimum) { 65 | throw new BadRequestException(`${name} should be greater than or equal to ${this.minimum}`); 66 | } 67 | 68 | return result; 69 | } 70 | } 71 | 72 | export const OffsetPipe = new IntegerPipe({ 73 | minimum: 0, 74 | optional: true, 75 | default: 0, 76 | }); 77 | 78 | export const LimitPipe = new IntegerPipe({ 79 | minimum: 1, 80 | maximum: 100, 81 | optional: true, 82 | default: 10, 83 | }); 84 | 85 | export const IdPipe = new IntegerPipe(); 86 | 87 | export const OptionalIdPipe = new IntegerPipe({ 88 | optional: true, 89 | }); 90 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/pipe/string.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, PipeTransform, Type } from '@nestjs/common'; 2 | 3 | export class StringPipeOptions { 4 | minLength?: number; 5 | maxLength?: number; 6 | whiteList?: string[]; 7 | regex?: RegExp; 8 | optional?: boolean; 9 | default?: string; 10 | } 11 | 12 | export class StringPipe implements PipeTransform { 13 | private readonly minLength?: number; 14 | private readonly maxLength?: number; 15 | private readonly whiteList?: Set; 16 | private readonly regex?: RegExp; 17 | private readonly optional?: boolean; 18 | private readonly default?: string; 19 | 20 | constructor(options: StringPipeOptions = {}) { 21 | this.minLength = this.checkIntegerOptionValue('minLength', options.minLength); 22 | this.maxLength = this.checkIntegerOptionValue('maxLength', options.maxLength); 23 | this.regex = options.regex; 24 | this.optional = options.optional; 25 | 26 | if (this.minLength > this.maxLength) { 27 | throw new Error('minLength should be less than or equal to maxLength'); 28 | } 29 | 30 | if (options.whiteList) { 31 | this.whiteList = new Set(options.whiteList); 32 | } 33 | 34 | if (typeof options.default === 'string') { 35 | this.check('default', options.default, Error); 36 | this.default = options.default; 37 | } 38 | } 39 | 40 | private checkIntegerOptionValue(optionName: string, value: number | undefined) { 41 | const type = typeof value; 42 | if (type === 'undefined') return; 43 | if (type !== 'number') throw new Error(`${optionName} should be a number or undefined`); 44 | if (!this.validateIsNonNegativeInteger(value)) 45 | throw new Error(`${optionName} should be an non-negative integer`); 46 | return value; 47 | } 48 | 49 | private validateIsNonNegativeInteger(value: number): boolean { 50 | return !isNaN(value) && Number.isInteger(value) && value >= 0; 51 | } 52 | 53 | private check(name: string, value: string, ErrorType: Type) { 54 | if (typeof this.minLength === 'number' && value.length < this.minLength) { 55 | throw new ErrorType( 56 | `The length of ${name} should be greater than or equal to ${this.minLength}` 57 | ); 58 | } 59 | 60 | if (typeof this.maxLength === 'number' && value.length > this.maxLength) { 61 | throw new ErrorType( 62 | `The length of ${name} should be less than or equal to ${this.maxLength}` 63 | ); 64 | } 65 | 66 | if (this.whiteList && !this.whiteList.has(value)) { 67 | throw new ErrorType(`${name} should be one of the white list`); 68 | } 69 | 70 | if (this.regex instanceof RegExp && !this.regex.test(value)) { 71 | throw new ErrorType(`${name} should match the regular expression pattern: ${this.regex}`); 72 | } 73 | } 74 | 75 | transform(value: any, metadata: ArgumentMetadata) { 76 | if (typeof value === 'undefined' && this.optional) return this.default ?? value; 77 | 78 | const { data, type } = metadata; 79 | const name = data ?? `a ${type}`; 80 | 81 | if (typeof value !== 'string') { 82 | throw new BadRequestException(`${name} should be a string`); 83 | } 84 | 85 | this.check(name, value, BadRequestException); 86 | return value; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/redis/redis-client/redis-client.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, DynamicModule } from '@nestjs/common'; 2 | import { RedisClientService } from './redis-client.service'; 3 | import { ConfigModule } from '../../config/config.module'; 4 | 5 | @Module({ 6 | imports: [ConfigModule], 7 | providers: [RedisClientService], 8 | exports: [RedisClientService], 9 | }) 10 | export class RedisClientModule {} 11 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/redis/redis-client/redis-client.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { RedisClientService } from './redis-client.service'; 3 | import { ConfigModule } from '../../config/config.module'; 4 | 5 | describe('RedisClientService', () => { 6 | let service: RedisClientService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | imports: [ 11 | ConfigModule 12 | ], 13 | providers: [ 14 | // RedisClientService 15 | ], 16 | }).compile(); 17 | 18 | // service = module.get(RedisClientService); 19 | }); 20 | 21 | it('should be defined', () => { 22 | expect(true).toBeDefined(); 23 | }); 24 | }); -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/redis/redis-client/redis-client.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '../../config/config.service'; 3 | import { createClient, RedisClient } from 'redis'; 4 | 5 | @Injectable() 6 | export class RedisClientService { 7 | private static client: RedisClient; 8 | constructor(private readonly configService: ConfigService) { 9 | RedisClientService.client = createClient(this.configService.redisPort, process.env.REDISHOST || this.configService.redisURL); 10 | } 11 | 12 | get client(): RedisClient { 13 | if (!RedisClientService.client) { 14 | RedisClientService.client = createClient(this.configService.redisPort, process.env.REDISHOST || this.configService.redisURL); 15 | } 16 | return RedisClientService.client; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/services/date-time/date-time.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { DateTimeService } from './date-time.service'; 3 | 4 | describe('DateTimeService', () => { 5 | let service: DateTimeService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ 10 | DateTimeService 11 | ], 12 | }).compile(); 13 | 14 | service = module.get(DateTimeService); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(service).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/services/date-time/date-time.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { DateTime } from 'luxon'; 3 | 4 | @Injectable() 5 | export class DateTimeService { 6 | 7 | private readonly logger = new Logger(DateTimeService.name); 8 | 9 | /** 10 | * 取得 Duel Revenge Index 11 | * 12 | * @private 13 | * @param {DateTime} nowDate 14 | * @returns {string} 15 | * @memberof GoddessDuelTaskService 16 | */ 17 | culcIndexByDuelRevenge(nowDate: DateTime): string { 18 | 19 | this.logger.log(`Week ${nowDate.weekday} Start Work!`); 20 | 21 | const year = nowDate.year 22 | const month = nowDate.month 23 | const weekOfYear = nowDate.weekNumber 24 | 25 | const indexData = `duel-revenge-report-${year}-${month}-${weekOfYear}`; 26 | this.logger.debug(`${indexData}`); 27 | 28 | return indexData; 29 | } 30 | 31 | /** 32 | * 取得一週的第一日 33 | * 34 | * @private 35 | * @param {DateTime} nowDate 36 | * @returns {DateTime} 37 | * @memberof GoddessDuelTaskService 38 | */ 39 | weekStart(nowDate: DateTime): DateTime { 40 | const weekDay = nowDate.weekday; 41 | 42 | // 週一就等於偏移(1 - 當週任意星期)(1 - n) 43 | const weekMonday = nowDate.plus({ day: 1 - weekDay }).set({ 44 | hour: 0, 45 | minute: 0, 46 | second: 0, 47 | millisecond: 0 48 | }); 49 | this.logger.debug(`lastWeekDate monday: ${weekMonday}`); 50 | return weekMonday; 51 | } 52 | 53 | /** 54 | * 取得一週的最後一日 55 | * 56 | * @private 57 | * @param {DateTime} nowDate 58 | * @returns {DateTime} 59 | * @memberof GoddessDuelTaskService 60 | */ 61 | weekEnd(nowDate: DateTime): DateTime { 62 | const weekday = nowDate.weekday; 63 | 64 | // 取得上週日 65 | // 週日就等於偏移(7- 當週任意星期)(7 - n)) 66 | const weekSunday = nowDate.plus({ day: 7 - weekday }).set({ 67 | hour: 22, 68 | minute: 0, 69 | second: 0, 70 | millisecond: 0 71 | }); 72 | this.logger.debug(`lastWeekDate sunday: ${weekSunday}`); 73 | return weekSunday; 74 | } 75 | 76 | /** 77 | * 根據星期取得一週的某一日 78 | * 79 | * @private 80 | * @param {DateTime} nowDate 81 | * @returns {DateTime} 82 | * @memberof GoddessDuelTaskService 83 | */ 84 | weekDateTime( 85 | nowDate: DateTime, 86 | targetWeekDay: number, 87 | h: number = 0, 88 | m: number = 0, 89 | s: number = 0, ): DateTime { 90 | const weekday = nowDate.weekday; 91 | 92 | // 取得上週日 93 | // 依照現在時間就等於偏移(targetWeekDay - n) or (n -targetWeekDay) 94 | const offset = weekday > targetWeekDay ? 95 | weekday - targetWeekDay : targetWeekDay - weekday 96 | const weekTarget = nowDate.plus({ day: offset }).set({ 97 | hour: h, 98 | minute: m, 99 | second: s, 100 | millisecond: 0 101 | }); 102 | this.logger.debug(`lastWeekDate sunday: ${weekTarget}`); 103 | return weekTarget; 104 | } 105 | 106 | /** 107 | * 取得 Duel Normal Index 108 | * 109 | * @private 110 | * @param {DateTime} nowDate 111 | * @returns {string} 112 | * @memberof GoddessDuelTaskService 113 | */ 114 | culcIndexByDuelNormal(nowDate: DateTime): string { 115 | 116 | this.logger.log(`Week ${nowDate.weekday} Start Work!`); 117 | 118 | const year = nowDate.year 119 | const month = nowDate.month 120 | const lastWeekOfYear = nowDate.weekNumber 121 | const indexData = `veve-duel-${year}-${month}-${lastWeekOfYear}`; 122 | 123 | this.logger.debug(`${indexData}`); 124 | return indexData; 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/services/service.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Logger } from '@nestjs/common'; 2 | import { UserService } from './user/user.service'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { AuthService } from '../auth/auth.service'; 5 | import { TypeOrmConfigService } from '../type-orm-config/type-orm-config.service'; 6 | import { AuthModule } from '../auth/auth.module'; 7 | 8 | @Module({ 9 | imports: [ 10 | TypeOrmModule.forFeature(TypeOrmConfigService.getEntities()), 11 | ], 12 | providers: [ 13 | Logger, 14 | UserService, 15 | ], 16 | exports: [ 17 | UserService, 18 | ], 19 | }) 20 | export class ServiceModule {} 21 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/services/user/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserService } from './user.service'; 3 | import { MockTestServiceModule } from '../../../test/mock-test-service.module'; 4 | import { ConfigService } from '../../config/config.service'; 5 | import path = require('path'); 6 | 7 | describe('UserService', () => { 8 | let service: UserService; 9 | 10 | beforeEach(async () => { 11 | const module: TestingModule = await Test.createTestingModule({ 12 | imports: [ 13 | MockTestServiceModule 14 | ], 15 | providers: [ 16 | { 17 | provide: ConfigService, 18 | useValue: new ConfigService(path.resolve('./', '.env', `development.env`)), 19 | }, 20 | UserService 21 | ] 22 | }).compile(); 23 | 24 | service = module.get(UserService); 25 | }); 26 | 27 | it('should be defined', () => { 28 | expect(service).toBeDefined(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/services/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class UserService { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/system-mailer/mailer-config/mailer-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { MailerOptionsFactory, MailerOptions } from '@nestjs-modules/mailer'; 3 | import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; 4 | import { ConfigService } from '../../config/config.service'; 5 | import * as path from 'path'; 6 | 7 | @Injectable() 8 | export class MailerConfigService implements MailerOptionsFactory { 9 | constructor(private readonly configService: ConfigService) {} 10 | 11 | async createMailerOptions(): Promise { 12 | const location = path.resolve(__dirname, '..', '..', 'assets', 'templates'); 13 | 14 | return { 15 | transport: this.configService.mailerSender, 16 | defaults: { 17 | from: this.configService.mailerSenderFrom, 18 | }, 19 | template: { 20 | dir: location, 21 | adapter: new HandlebarsAdapter(), 22 | options: { 23 | strict: true, 24 | }, 25 | }, 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/system-mailer/system-mailer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MailerModule } from '@nestjs-modules/mailer'; 3 | import { MailerConfigService } from './mailer-config/mailer-config.service'; 4 | import { ConfigModule } from '../config/config.module'; 5 | import { SystemMailerService } from './system-mailer.service'; 6 | import { APPWinstonModule } from '../winston/appwinston.module'; 7 | 8 | @Module({ 9 | imports: [ 10 | MailerModule.forRootAsync({ 11 | imports: [ConfigModule], 12 | useClass: MailerConfigService, 13 | }), 14 | APPWinstonModule, 15 | ], 16 | providers: [SystemMailerService], 17 | exports: [SystemMailerService], 18 | }) 19 | export class SystemMailerModule {} 20 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/system-mailer/system-mailer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { MailerService } from '@nestjs-modules/mailer'; 3 | import path = require('path'); 4 | import url = require('url'); 5 | import { 6 | LoggerHelperService, 7 | TrackerLogger, 8 | TrackerLoggerCreator, 9 | } from '../winston/logger-helper.service'; 10 | import { ConfigService } from '../config/config.service'; 11 | 12 | interface CcAccountInformMailParams { 13 | title: string; 14 | itemName1: string; 15 | itemValue1: string; 16 | itemName2: string; 17 | itemValue2: string; 18 | } 19 | 20 | const TURING_CHAIN_LOGO_ATTACHMENT = { 21 | filename: 'turing-logo@3x.png', 22 | path: path.resolve(__dirname, '..', 'assets', 'images', 'turing-logo@3x.png'), 23 | cid: 'imagename', 24 | }; 25 | 26 | @Injectable() 27 | export class SystemMailerService { 28 | private readonly trackerLoggerCreator: TrackerLoggerCreator; 29 | private readonly ccCoWebLoginPageUrl: string; 30 | private readonly copWebLoginPageUrl: string; 31 | private readonly customerService: string = 'service@turingchain.tech'; 32 | 33 | constructor( 34 | private readonly mailerService: MailerService, 35 | private readonly configService: ConfigService, 36 | loggerHelperService: LoggerHelperService 37 | ) { 38 | this.trackerLoggerCreator = loggerHelperService.makeCreator(SystemMailerService.name); 39 | this.ccCoWebLoginPageUrl = url.resolve(configService.ccCoWebHost, 'login'); 40 | this.copWebLoginPageUrl = url.resolve(configService.copWebHost, 'login'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/type-orm-config/type-orm-config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { ConfigModule } from '../config/config.module'; 4 | import { ConfigService } from '../config/config.service'; 5 | import { TypeOrmConfigService } from './type-orm-config.service'; 6 | 7 | @Module({ 8 | imports: [ 9 | TypeOrmModule.forRootAsync({ 10 | imports: [ConfigModule], 11 | inject: [ 12 | // 宣告哪個provider或是service需要被注入 13 | ConfigService, 14 | ], 15 | useClass: TypeOrmConfigService, 16 | }), 17 | ], 18 | exports: [ 19 | TypeOrmModule 20 | ] 21 | }) 22 | export class TypeOrmConfigModule { } 23 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/type-orm-config/type-orm-config.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TypeOrmConfigService } from './type-orm-config.service'; 3 | import { ConfigService } from '../config/config.service'; 4 | import * as path from 'path'; 5 | 6 | const ENV = process.env.NODE_ENV || 'development'; 7 | 8 | describe('TypeOrmConfigService', () => { 9 | let service: TypeOrmConfigService; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | providers: [{ 14 | provide: ConfigService, 15 | useValue: new ConfigService(path.resolve('./', '.env', `${ENV}.env`)), 16 | }, 17 | TypeOrmConfigService, 18 | ], 19 | }).compile(); 20 | 21 | service = module.get(TypeOrmConfigService); 22 | }); 23 | 24 | it('should be defined', () => { 25 | expect(service).toBeDefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/type-orm-config/type-orm-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { TypeOrmOptionsFactory, TypeOrmModuleOptions } from '@nestjs/typeorm'; 3 | import { ConfigService } from '../config/config.service'; 4 | 5 | @Injectable() 6 | export class TypeOrmConfigService implements TypeOrmOptionsFactory { 7 | 8 | private readonly logger = new Logger(TypeOrmConfigService.name); 9 | constructor(private readonly configService: ConfigService) { } 10 | 11 | createTypeOrmOptions(): TypeOrmModuleOptions { 12 | return { 13 | type: this.configService.dbType as any, 14 | host: this.configService.dbHost, 15 | port: this.configService.dbPort, 16 | username: this.configService.dbUser, 17 | password: this.configService.dbPassword, 18 | database: this.configService.dbName, 19 | entities: TypeOrmConfigService.getEntities(), 20 | synchronize: this.configService.dbSYNC, 21 | logging: this.configService.dbLogging, 22 | }; 23 | } 24 | 25 | static getEntities() { 26 | return [ 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/winston/__mocks__/logger-helper.service.ts: -------------------------------------------------------------------------------- 1 | import { LoggerService } from '@nestjs/common'; 2 | 3 | // Reset all 4 | export function mockClearAll() { 5 | // Reset mockedLogger 6 | Object.values(innerMockedLogger).forEach(m => m.mockClear()); 7 | 8 | // Reset LoggerHelperService 9 | mockedLoggerHelperSerivceMakeCreator.mockClear(); 10 | LoggerHelperService.mockClear(); 11 | 12 | // Reset TrackerLoggerCreator 13 | mockedTrackerLoggerCreatorCreate.mockClear(); 14 | TrackerLoggerCreator.mockClear(); 15 | 16 | // Reset TrackerLogger 17 | TrackerLogger.mockClear(); 18 | } 19 | 20 | // Default mocked Logger 21 | const innerMockedLogger = { 22 | log: jest.fn(), 23 | error: jest.fn(), 24 | warn: jest.fn(), 25 | debug: jest.fn(), 26 | verbose: jest.fn(), 27 | }; 28 | 29 | export const mockedLogger: LoggerService = innerMockedLogger; 30 | 31 | // class LoggerHelperService 32 | export const mockedLoggerHelperSerivceMakeCreator = jest.fn(function (name: string) { 33 | return new TrackerLoggerCreator(mockedLogger, name); 34 | }); 35 | 36 | export const LoggerHelperService = jest.fn().mockImplementation(() => { 37 | return { 38 | makeCreator: mockedLoggerHelperSerivceMakeCreator, 39 | }; 40 | }); 41 | 42 | // class TrackerLoggerCreator 43 | export const mockedTrackerLoggerCreatorCreate = jest.fn(function (trackingId: string) { 44 | return new TrackerLogger(this.loggerService, this.name, trackingId); 45 | }); 46 | 47 | export const TrackerLoggerCreator = jest.fn().mockImplementation((logger: LoggerService, name: string) => { 48 | return { 49 | name, 50 | loggerService: logger, 51 | create: mockedTrackerLoggerCreatorCreate, 52 | }; 53 | }); 54 | 55 | // class TrackerLogger 56 | export const TrackerLogger = jest 57 | .fn() 58 | .mockImplementation((loggerService: LoggerService, name: string, _trackingId: string) => { 59 | function createMessage(message: any) { 60 | return { 61 | trackingId: _trackingId, 62 | message: `${name}: ${message}`, 63 | }; 64 | } 65 | 66 | const mocked = { 67 | name, 68 | loggerService, 69 | _trackingId, 70 | createMessage, 71 | log: (message: any, context?: string) => loggerService.log(createMessage(message), context), 72 | error: (message: any, context?: string) => loggerService.error(createMessage(message), context), 73 | warn: (message: any, context?: string) => loggerService.warn(createMessage(message), context), 74 | debug: (message: any, context?: string) => loggerService.debug(createMessage(message), context), 75 | verbose: (message: any, context?: string) => loggerService.verbose(createMessage(message), context), 76 | }; 77 | 78 | Object.defineProperty(mocked, 'trackingId', { 79 | get: function () { 80 | return this._trackingId; 81 | }, 82 | }); 83 | 84 | return mocked; 85 | }); 86 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/winston/appwinston.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import winston = require('winston'); 3 | import { 4 | utilities as nestWinstonModuleUtilities, 5 | WinstonModule, 6 | } from 'nest-winston'; 7 | import { ConfigService } from '../config/config.service'; 8 | import { LoggerHelperService } from './logger-helper.service'; 9 | 10 | const prettyJson = winston.format.printf((info) => { 11 | if (info?.message?.constructor === Object) { 12 | info.message = JSON.stringify(info.message, null, 4); 13 | } 14 | return `${info.level}: ${info.message}`; 15 | }); 16 | 17 | @Module({ 18 | imports: [ 19 | WinstonModule.forRootAsync({ 20 | useFactory: (configServices: ConfigService) => ({ 21 | level: configServices.logLevel, 22 | transports: [ 23 | new winston.transports.Console({ 24 | format: winston.format.combine( 25 | winston.format.prettyPrint(), 26 | winston.format.splat(), 27 | winston.format.simple(), 28 | prettyJson, 29 | winston.format.timestamp(), 30 | nestWinstonModuleUtilities.format.nestLike(), 31 | ), 32 | }), 33 | ], 34 | }), 35 | inject: [ConfigService], 36 | }), 37 | ], 38 | providers: [LoggerHelperService], 39 | exports: [LoggerHelperService], 40 | }) 41 | export class APPWinstonModule {} 42 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/src/winston/logger-helper.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, LoggerService } from '@nestjs/common'; 2 | import { InjectWinstonLoggerService } from '../decorators/inject-winston-logger-service.decorator'; 3 | 4 | @Injectable() 5 | export class LoggerHelperService { 6 | constructor( 7 | @InjectWinstonLoggerService() 8 | private readonly loggerService: LoggerService 9 | ) {} 10 | 11 | /** 12 | * 建立有冠上領域名稱的 `Logger` 13 | * @param name 領域名稱,如:CopAccount、Image 等等 14 | */ 15 | makeCreator(name?: string): TrackerLoggerCreator { 16 | return new TrackerLoggerCreator(this.loggerService, name); 17 | } 18 | } 19 | 20 | /** 21 | * 封裝 `trackingId` 的建立者。 22 | * 此類別會先初步封裝來自外部的 `LoggerService` 以及領域名稱 23 | */ 24 | export class TrackerLoggerCreator { 25 | /** 26 | * @param loggerService 來自外部的 `LoggerService` 27 | * @param name 領域名稱,如:CopAccount、Image 等等 28 | */ 29 | constructor(private readonly loggerService: LoggerService, private readonly name: string) {} 30 | 31 | create(trackingId: string) { 32 | return new TrackerLogger(this.loggerService, this.name, trackingId); 33 | } 34 | } 35 | 36 | /** 37 | * 原則上經由 `TrackerLoggerCreator` 帶入 `trackingId` 並建成可用的 `Logger` 38 | */ 39 | export class TrackerLogger implements LoggerService { 40 | /** 41 | * @param loggerService 來自外部的 LoggerService 42 | * @param name 領域名稱,如:CopAccount、Image 等等 43 | */ 44 | constructor( 45 | private readonly loggerService: LoggerService, 46 | private readonly name: string, 47 | private readonly _trackingId: string 48 | ) {} 49 | 50 | get trackingId(): string { 51 | return this._trackingId; 52 | } 53 | 54 | /** 55 | * 為訊息冠上領域名稱前綴與追蹤 Id 56 | * @param message 訊息 57 | */ 58 | private createMessage = (message: any) => { 59 | return { 60 | trackingId: this.trackingId, 61 | message: `${this.name}: ${message}`, 62 | }; 63 | }; 64 | 65 | log = (message: any, context?: string) => { 66 | this.loggerService.log(this.createMessage(message), context); 67 | }; 68 | 69 | error = (message: any, trace?: string, context?: string) => { 70 | this.loggerService.error(this.createMessage(message), trace, context); 71 | }; 72 | 73 | warn = (message: any, context?: string) => { 74 | this.loggerService.warn(this.createMessage(message), context); 75 | }; 76 | 77 | debug? = (message: any, context?: string) => { 78 | this.loggerService.debug(this.createMessage(message), context); 79 | }; 80 | 81 | verbose? = (message: any, context?: string) => { 82 | this.loggerService.verbose(this.createMessage(message), context); 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from '../src/app.module'; 4 | 5 | describe('AppController (e2e)', () => { 6 | let app; 7 | 8 | beforeEach(async () => { 9 | const moduleFixture: TestingModule = await Test.createTestingModule({ 10 | imports: [AppModule], 11 | }).compile(); 12 | 13 | app = moduleFixture.createNestApplication(); 14 | await app.init(); 15 | }); 16 | 17 | it('/ (GET)', () => { 18 | return request(app.getHttpServer()) 19 | .get('/') 20 | .expect(200) 21 | .expect('Hello World!'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/test/mock-test-service.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Logger } from '@nestjs/common'; 2 | import { getRepositoryToken } from '@nestjs/typeorm'; 3 | import { AuthModule } from '../src/auth/auth.module'; 4 | 5 | const providers = [ 6 | Logger, 7 | ] 8 | 9 | const imports = [ 10 | 11 | ] 12 | 13 | @Module({ 14 | imports: imports, 15 | providers: providers, 16 | exports: providers, 17 | }) 18 | export class MockTestServiceModule { } -------------------------------------------------------------------------------- /src/templates/nestjs-general/test/mock-test.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Logger } from '@nestjs/common'; 2 | import { ConfigService } from '../src/config/config.service'; 3 | import path = require('path'); 4 | import { DateTimeService } from '../src/services/date-time/date-time.service'; 5 | import { ElasticsearchConfigService } from '../src/elasticsearch-config/elasticsearch-config.service'; 6 | import { RedisClientService } from '../src/redis/redis-client/redis-client.service'; 7 | import { UserService } from '../src/services/user/user.service'; 8 | import { AuthService } from '../src/auth/auth.service'; 9 | import { MockTestServiceModule } from './mock-test-service.module'; 10 | 11 | const providers = [ 12 | Logger, 13 | { 14 | provide: ConfigService, 15 | useValue: new ConfigService(path.resolve('./', '.env', `development.env`)), 16 | }, 17 | DateTimeService, 18 | // ElasticsearchConfigService, 19 | RedisClientService, 20 | UserService, 21 | ] 22 | 23 | @Module({ 24 | imports: [ 25 | MockTestServiceModule, 26 | ], 27 | providers: providers, 28 | exports: providers, 29 | }) 30 | export class MockTestModule { } -------------------------------------------------------------------------------- /src/templates/nestjs-general/test/mockRepository.ts: -------------------------------------------------------------------------------- 1 | export const mockRepository = jest.fn(() => ({ 2 | metadata: { 3 | columns: [], 4 | relations: [], 5 | }, 6 | })); 7 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "cdk-k8s"] 4 | } 5 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist", "cdk-k8s"] 15 | } 16 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false, 16 | "no-console": false, 17 | "max-classes-per-file": false, 18 | "object-literal-shorthand": false, 19 | "no-unused-expression": false 20 | }, 21 | "rulesDirectory": [] 22 | } 23 | -------------------------------------------------------------------------------- /src/templates/nestjs-general/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = { 6 | entry: ['webpack/hot/poll?100', './src/main.ts'], 7 | watch: true, 8 | target: 'node', 9 | externals: [ 10 | nodeExternals({ 11 | whitelist: ['webpack/hot/poll?100'], 12 | }), 13 | ], 14 | module: { 15 | rules: [ 16 | { 17 | test: /.tsx?$/, 18 | use: 'ts-loader', 19 | exclude: /node_modules/, 20 | }, 21 | ], 22 | }, 23 | mode: 'development', 24 | resolve: { 25 | extensions: ['.tsx', '.ts', '.js'], 26 | }, 27 | plugins: [new webpack.HotModuleReplacementPlugin()], 28 | output: { 29 | path: path.join(__dirname, 'dist'), 30 | filename: 'server.js', 31 | }, 32 | }; -------------------------------------------------------------------------------- /src/utils/template.ts: -------------------------------------------------------------------------------- 1 | import * as ejs from 'ejs'; 2 | 3 | export interface TemplateData { 4 | projectName: string 5 | } 6 | 7 | export function render(content: string, data: TemplateData) { 8 | return ejs.render(content, data); 9 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "outDir": "dist", 5 | "target": "es5", 6 | "lib": ["es6", "dom"], 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "moduleResolution": "node", 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "noUnusedLocals": true 17 | }, 18 | "exclude": [ 19 | "node_modules", 20 | "dist", 21 | "src/templates" 22 | ] 23 | } --------------------------------------------------------------------------------