diff --git a/.gitignore b/.gitignore index f37c1a6..20761d0 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,4 @@ build/ env.js package-lock.json test -yarn.lock \ No newline at end of file +yarn.lock diff --git a/.travis.yml b/.travis.yml index b9f070c..d78fad9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: node_js node_js: - - 8 - 10 install: @@ -18,5 +17,7 @@ jobs: deploy: provider: script skip_cleanup: true + on: + branch: master script: - npm run release diff --git a/README.md b/README.md index 5aa5161..f9a495b 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,110 @@ -[![Serverless Components](https://img.serverlesscloud.cn/2020210/1581352135771-express.png)](http://serverless.com) +# Serverless Nest.js -
+**腾讯云 Nest.js 组件** ⎯⎯⎯ 通过使用 [Serverless Framework](https://github.com/serverless/components/tree/cloud),基于云上 Serverless 服务(如网关、云函数等),实现“0”配置,便捷开发,极速部署你的 Nest.js 应用。 -**腾讯云 Express 组件** ⎯⎯⎯ 通过使用 [Tencent Serverless Framework](https://github.com/serverless/components/tree/cloud),基于云上 Serverless 服务(如网关、云函数等),实现“0”配置,便捷开发,极速部署你的 express 应用,Express 组件支持丰富的配置扩展,提供了目前最易用、低成本并且弹性伸缩的 Experss 项目开发/托管能力。 -
+> 注意:本项目仅支持使用 [ExpressAdapter](https://docs.nestjs.com/faq/http-adapter) 的 Nest.js 项目。 -特性介绍: +### 安装 -- [x] **按需付费** - 按照请求的使用量进行收费,没有请求时无需付费 -- [x] **"0"配置** - 只需要关心项目代码,之后部署即可,Serverless Framework 会搞定所有配置。 -- [x] **极速部署** - 仅需几秒,部署你的整个 express 应用。 -- [x] **实时日志** - 通过实时日志的输出查看业务状态,便于直接在云端开发应用。 -- [x] **云端调试** - 针对 Node.js 框架支持一键云端调试能力,屏蔽本地环境的差异。 -- [x] **便捷协作** - 通过云端的状态信息和部署日志,方便的进行多人协作开发。 -- [x] **自定义域名** - 支持配置自定义域名及 HTTPS 访问 +通过 npm 安装最新版本的 Serverless Framework -
+```bash +$ npm install -g serverless +``` - +### 创建 -快速开始: +通过如下命令和模板链接,快速创建一个 Nest.js 应用: -1. [**安装**](#1-安装) -2. [**创建**](#2-创建) -3. [**部署**](#3-部署) -4. [**配置**](#4-配置) -5. [**开发调试**](#5-开发调试) -6. [**查看状态**](#6-查看状态) -7. [**移除**](#7-移除) +```bash +$ npm i -g @nestjs/cli +$ nest new serverless-nestjs +$ cd serverless-nestjs +``` -更多资源: +执行如下命令,安装应用的对应依赖 -- [**架构说明**](#架构说明) -- [**账号配置**](#账号配置) +``` +$ npm install +``` -  +### 项目改造 -### 1. 安装 +1. 由于云端函数执行时不需要监听端口的,所以我们需要修改下入口文件 `src/main.ts`,如下: -通过 npm 安装最新版本的 Serverless Framework +```typescript +import { NestFactory } from '@nestjs/core' +import { join } from 'path' +import { AppModule } from './app.module' -```bash -$ npm install -g serverless -``` +async function bootstrap() { + const app = await NestFactory.create(AppModule) -### 2. 创建 + return app +} -通过如下命令和模板链接,快速创建一个 express 应用: +// 注意: 通过注入 NODE_ENV 为 local,来方便本地启动服务,进行开发调试 +const isLocal = process.env.NODE_ENV === 'local' +if (isLocal) { + bootstrap().then((app) => { + app.listen(3000, () => { + console.log(`Server start on http://localhost:3000`) + }) + }) +} -```bash -$ serverless create --template-url https://github.com/serverless-components/tencent-express/tree/v2/serverless-express -$ cd serverless-express +// 导出启动函数,给 sls.js 使用 +export { bootstrap } ``` -执行如下命令,安装 express 应用的对应依赖 +2. 项目根目录下新增 `sls.js` 文件,用来提供给 Nest.js 组件使用: +```js +// 注意: 根据实际项目入口文件名进行修改,此 demo 为默认情况 +const { bootstrap } = require('./dist/main') + +module.exports = bootstrap ``` -$ npm install -``` -### 3. 部署 +> 注意:实际开发可根据个人项目路径,来导出启动函数 `bootstrap`。 + +### 部署 -在 `serverless.yml` 文件下的目录中运行 `serverless deploy` 进行 express 项目的部署。第一次部署可能耗时相对较久,但后续的二次部署会在几秒钟之内完成。部署完毕后,你可以在命令行的输出中查看到你 express 应用的 URL 地址,点击地址即可访问你的 express 项目。 +由于 Nest.js 项目是 TypeScript,部署前需要编译成 JavaScript,在项目中执行编译命令即可: -**注意:** +```bash +$ npm run build +``` -如您的账号未[登陆](https://cloud.tencent.com/login)或[注册](https://cloud.tencent.com/register)腾讯云,您可以直接通过`微信`扫描命令行中的二维码进行授权登陆和注册。 +在 `serverless.yml` 文件下的目录中运行 `serverless deploy` 进行 Nest.js 项目的部署。第一次部署可能耗时相对较久,但后续的二次部署会在几秒钟之内完成。部署完毕后,你可以在命令行的输出中查看到你 nestjs 应用的 URL 地址,点击地址即可访问你的 Nest.js 项目。 -如果出现了 `internal server error` 的报错,请检查是否在创建模板后没有运行 `npm install`。 +> **注意:** 如您的账号未 [登录](https://cloud.tencent.com/login) 或 [注册](https://cloud.tencent.com/register) 腾讯云,您可以直接通过 `微信` 扫描命令行中的二维码进行授权登陆和注册。 如果希望查看更多部署过程的信息,可以通过`sls deploy --debug` 命令查看部署过程中的实时日志信息,`sls`是 `serverless` 命令的缩写。 -
- -### 4. 配置 +### 配置 -Express 组件支持 0 配置部署,也就是可以直接通过配置文件中的默认值进行部署。但你依然可以修改更多可选配置来进一步开发该 Express 项目。 +nestjs 组件支持 0 配置部署,也就是可以直接通过配置文件中的默认值进行部署。但你依然可以修改更多可选配置来进一步开发该 nestjs 项目。 -以下是 Express 组件的 `serverless.yml`完整配置说明: +以下是 nestjs 组件的 `serverless.yml` 简单配置示例: ```yml # serverless.yml -component: express # (required) name of the component. In that case, it's express. -name: expressDemo # (required) name of your express component instance. +component: nestjs # (required) name of the component. In that case, it's nestjs. +name: nestjsDemo # (required) name of your nestjs component instance. org: orgDemo # (optional) serverless dashboard org. default is the first org you created during signup. app: appDemo # (optional) serverless dashboard app. default is the same as the name property. stage: dev # (optional) serverless dashboard stage. default is dev. inputs: - src: ./ # (optional) path to the source folder. default is a hello world app. - functionName: expressDemo + src: + src: ./ # (optional) path to the source folder. default is a hello world app. + exclude: + - .env + functionName: nestjsDemo region: ap-guangzhou runtime: Nodejs10.15 - exclude: - - .env apigatewayConf: protocols: - http @@ -102,13 +112,13 @@ inputs: environment: release ``` -点此查看[全量配置及配置说明](https://github.com/serverless-components/tencent-express/blob/v2/docs/configure.md) +点此查看[全量配置及配置说明](https://github.com/serverless-components/tencent-nestjs/tree/master/docs/configure.md) 当你根据该配置文件更新配置字段后,再次运行 `serverless deploy` 或者 `serverless` 就可以更新配置到云端。 -### 5. 开发调试 +### 远程调试云函数 -部署了 Express.js 应用后,可以通过开发调试能力对该项目进行二次开发,从而开发一个生产应用。在本地修改和更新代码后,不需要每次都运行 `serverless deploy` 命令来反复部署。你可以直接通过 `serverless dev` 命令对本地代码的改动进行检测和自动上传。 +部署了 nestjs.js 应用后,可以通过开发调试能力对该项目进行二次开发,从而开发一个生产应用。在本地修改和更新代码后,不需要每次都运行 `serverless deploy` 命令来反复部署。你可以直接通过 `serverless dev` 命令对本地代码的改动进行检测和自动上传。 可以通过在 `serverless.yml`文件所在的目录下运行 `serverless dev` 命令开启开发调试能力。 @@ -116,7 +126,7 @@ inputs: 除了实时日志输出之外,针对 Node.js 应用,当前也支持云端调试能力。在开启 `serverless dev` 命令之后,将会自动监听远端端口,并将函数的超时时间临时配置为 900s。此时你可以通过访问 chrome://inspect/#devices 查找远端的调试路径,并直接对云端代码进行断点等调试。在调试模式结束后,需要再次部署从而将代码更新并将超时时间设置为原来的值。详情参考[开发模式和云端调试](https://cloud.tencent.com/document/product/1154/43220)。 -### 6. 查看状态 +### 查看状态 在`serverless.yml`文件所在的目录下,通过如下命令查看部署状态: @@ -124,9 +134,9 @@ inputs: $ serverless info ``` -### 7. 移除 +### 移除 -在`serverless.yml`文件所在的目录下,通过以下命令移除部署的 Express 服务。移除后该组件会对应删除云上部署时所创建的所有相关资源。 +在`serverless.yml`文件所在的目录下,通过以下命令移除部署的 nestjs 服务。移除后该组件会对应删除云上部署时所创建的所有相关资源。 ``` $ serverless remove @@ -134,16 +144,6 @@ $ serverless remove 和部署类似,支持通过 `sls remove --debug` 命令查看移除过程中的实时日志信息,`sls`是 `serverless` 命令的缩写。 -## 架构说明 - -Express 组件将在腾讯云账户中使用到如下 Serverless 服务: - -- [x] **API 网关** - API 网关将会接收外部请求并且转发到 SCF 云函数中。 -- [x] **SCF 云函数** - 云函数将承载 Express.js 应用。 -- [x] **CAM 访问控制** - 该组件会创建默认 CAM 角色用于授权访问关联资源。 -- [x] **COS 对象存储** - 为确保上传速度和质量,云函数压缩并上传代码时,会默认将代码包存储在特定命名的 COS 桶中。 -- [x] **SSL 证书服务** - 如果你在 yaml 文件中配置了 `apigatewayConf.customDomains` 字段,需要做自定义域名绑定并开启 HTTPS 时,也会用到证书管理服务和域名服务。Serverless Framework 会根据已经备案的域名自动申请并配置 SSL 证书。 - ## 账号配置 当前默认支持 CLI 扫描二维码登录,如您希望配置持久的环境变量/秘钥信息,也可以本地创建 `.env` 文件 diff --git a/docs/configure.md b/docs/configure.md index 14a7eca..969ec4a 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -5,15 +5,15 @@ ```yml # serverless.yml -component: express # (必选) 组件名称,在该实例中为express -name: expressDemo # 必选) 组件实例名称. +component: nestjs # (必选) 组件名称,在该实例中为nestjs +name: nestjsDemo # 必选) 组件实例名称. org: orgDemo # (可选) 用于记录组织信息,默认值为您的腾讯云账户 appid,必须为字符串 app: appDemo # (可选) 用于记录组织信息. 默认与name相同,必须为字符串 stage: dev # (可选) 用于区分环境信息,默认值是 dev inputs: region: ap-guangzhou # 云函数所在区域 - functionName: expressDemo # 云函数名称 + functionName: nestjsDemo # 云函数名称 serviceName: mytest # api网关服务名称 runtime: Nodejs10.15 # 运行环境 serviceId: service-np1uloxw # api网关服务ID @@ -32,19 +32,22 @@ inputs: version: 1 # 版本 functionConf: # 函数配置相关 timeout: 10 # 超时时间,单位秒 + eip: false # 是否固定出口IP memorySize: 128 # 内存大小,单位MB environment: # 环境变量 variables: # 环境变量数组 TEST: vale vpcConfig: # 私有网络配置 - subnetId: '' # 私有网络的Id - vpcId: '' # 子网ID + vpcId: '' # 私有网络的Id + subnetId: '' # 子网ID apigatewayConf: # api网关配置 isDisabled: false # 是否禁用自动创建 API 网关功能 enableCORS: true # 允许跨域 customDomains: # 自定义域名绑定 - domain: abc.com # 待绑定的自定义的域名 certificateId: abcdefg # 待绑定自定义域名的证书唯一 ID + # 如要设置自定义路径映射,请设置为 false + isDefaultMapping: false # 自定义路径映射的路径。使用自定义映射时,可一次仅映射一个 path 到一个环境,也可映射多个 path 到多个环境。并且一旦使用自定义映射,原本的默认映射规则不再生效,只有自定义映射路径生效。 pathMappingSet: - path: / @@ -56,13 +59,13 @@ inputs: - http - https environment: test + serviceTimeout: 15 usagePlan: # 用户使用计划 usagePlanId: 1111 usagePlanName: slscmp usagePlanDesc: sls create maxRequestNum: 1000 auth: # 密钥 - serviceTimeout: 15 secretName: secret secretIds: - xxx @@ -74,15 +77,13 @@ inputs: | 参数名称 | 是否必选 | 默认值 | 描述 | | ------------------------------------ | :------: | :-------------: | :------------------------------------------------------------------ | -| runtime | 否 | Nodejs10.15 | 执行环境, 目前支持: Nodejs6.10, Nodejs8.9, Nodejs10.15, Nodejs12.16 | -| region | 否 | ap-guangzhou | 项目部署所在区域,默认广州区 | +| runtime | 否 | `Nodejs10.15` | 执行环境, 目前支持: Nodejs6.10, Nodejs8.9, Nodejs10.15, Nodejs12.16 | +| region | 否 | `ap-guangzhou` | 项目部署所在区域,默认广州区 | | functionName | 否 | | 云函数名称 | | serviceName | 否 | | API 网关服务名称, 默认创建一个新的服务名称 | | serviceId | 否 | | API 网关服务 ID,如果存在将使用这个 API 网关服务 | | src | 否 | `process.cwd()` | 默认为当前目录, 如果是对象, 配置参数参考 [执行目录](#执行目录) | | layers | 否 | | 云函数绑定的 layer, 配置参数参考 [层配置](#层配置) | -| exclude | 否 | | 不包含的文件 | -| include | 否 | | 包含的文件, 如果是相对路径,是相对于 `serverless.yml`的路径 | | [functionConf](#函数配置) | 否 | | 函数配置 | | [apigatewayConf](#API-网关配置) | 否 | | API 网关配置 | | [cloudDNSConf](#DNS-配置) | 否 | | DNS 配置 | @@ -90,12 +91,12 @@ inputs: ## 执行目录 -| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | -| -------- | :------: | :-------------: | :----: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| src | 否 | String | | 代码路径。与 object 不能同时存在。 | -| exclude | 否 | Array of String | | 不包含的文件或路径, 遵守 [glob 语法](https:# github.com/isaacs/node-glob) | -| bucket | 否 | String | | bucket 名称。如果配置了 src,表示部署 src 的代码并压缩成 zip 后上传到 bucket-appid 对应的存储桶中;如果配置了 object,表示获取 bucket-appid 对应存储桶中 object 对应的代码进行部署。 | -| object | 否 | String | | 部署的代码在存储桶中的路径。 | +| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | +| -------- | :------: | :------: | :----: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| src | 否 | String | | 代码路径。与 object 不能同时存在。 | +| exclude | 否 | String[] | | 不包含的文件或路径, 遵守 [glob 语法](https://github.com/isaacs/node-glob) | +| bucket | 否 | String | | bucket 名称。如果配置了 src,表示部署 src 的代码并压缩成 zip 后上传到 bucket-appid 对应的存储桶中;如果配置了 object,表示获取 bucket-appid 对应存储桶中 object 对应的代码进行部署。 | +| object | 否 | String | | 部署的代码在存储桶中的路径。 | ## 层配置 @@ -106,11 +107,11 @@ inputs: ### DNS 配置 -参考: https:# cloud.tencent.com/document/product/302/8516 +参考: https://cloud.tencent.com/document/product/302/8516 | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | | ---------- | :------: | -------- | :----: | :---------------------------------------------- | -| ttl | 否 | Number | 600 | TTL 值,范围 1 - 604800,不同等级域名最小值不同 | +| ttl | 否 | Number | `600` | TTL 值,范围 1 - 604800,不同等级域名最小值不同 | | recordLine | 否 | String[] | | 记录的线路名称 | ### 指定区配置 @@ -119,18 +120,18 @@ inputs: | ------------------------------- | :------: | ------ | ------ | ------------ | | [functionConf](#函数配置) | 否 | Object | | 函数配置 | | [apigatewayConf](#API-网关配置) | 否 | Object | | API 网关配置 | -| [cloudDNSConf](#DNS-配置) | 否 | Object | | DNS 配置 | ### 函数配置 -参考: https:# cloud.tencent.com/document/product/583/18586 +参考: https://cloud.tencent.com/document/product/583/18586 -| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | -| ----------- | :------: | :----: | :----: | :------------------------------------------------------------------------------ | -| timeout | 否 | Number | 3 | 函数最长执行时间,单位为秒,可选值范围 1-900 秒,默认为 3 秒 | -| memorySize | 否 | Number | 128 | 函数运行时内存大小,默认为 128M,可选范围 64、128MB-3072MB,并且以 128MB 为阶梯 | -| environment | 否 | Object | | 函数的环境变量, 参考 [环境变量](#环境变量) | -| vpcConfig | 否 | Object | | 函数的 VPC 配置, 参考 [VPC 配置](#VPC-配置) | +| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | +| ----------- | :------: | :-----: | :-----: | :------------------------------------------------------------------------------ | +| timeout | 否 | Number | `3` | 函数最长执行时间,单位为秒,可选值范围 1-900 秒,默认为 3 秒 | +| memorySize | 否 | Number | `128` | 函数运行时内存大小,默认为 128M,可选范围 64、128MB-3072MB,并且以 128MB 为阶梯 | +| environment | 否 | Object | | 函数的环境变量, 参考 [环境变量](#环境变量) | +| vpcConfig | 否 | Object | | 函数的 VPC 配置, 参考 [VPC 配置](#VPC-配置) | +| eip | 否 | Boolean | `false` | 是否固定出口 IP | ##### 环境变量 @@ -147,30 +148,31 @@ inputs: ### API 网关配置 -| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | -| ------------ | :------: | :------- | :------- | :--------------------------------------------------------------------------------- | -| protocols | 否 | String[] | ['http'] | 前端请求的类型,如 http,https,http 与 https | -| environment | 否 | String | release | 发布环境. 目前支持三种发布环境: test(测试), prepub(预发布) 与 release(发布). | -| usagePlan | 否 | | | 使用计划配置, 参考 [使用计划](#使用计划) | -| auth | 否 | | | API 密钥配置, 参考 [API 密钥](#API-密钥配置) | -| customDomain | 否 | Object[] | | 自定义 API 域名配置, 参考 [自定义域名](#自定义域名) | -| enableCORS | 否 | Boolean | `false` | 开启跨域。默认值为否。 | -| isDisabled | 否 | Boolean | `false` | 关闭自动创建 API 网关功能。默认值为否,即默认自动创建 API 网关。 | +| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | +| -------------- | :------: | :------- | :--------- | :--------------------------------------------------------------------------------- | +| protocols | 否 | String[] | `['http']` | 前端请求的类型,如 http,https,http 与 https | +| environment | 否 | String | `release` | 发布环境. 目前支持三种发布环境: test(测试), prepub(预发布) 与 release(发布). | +| usagePlan | 否 | | | 使用计划配置, 参考 [使用计划](#使用计划) | +| auth | 否 | | | API 密钥配置, 参考 [API 密钥](#API-密钥配置) | +| customDomain | 否 | Object[] | | 自定义 API 域名配置, 参考 [自定义域名](#自定义域名) | +| enableCORS | 否 | Boolean | `false` | 开启跨域。默认值为否。 | +| serviceTimeout | 否 | Number | `15` | Api 超时时间,单位: 秒 | +| isDisabled | 否 | Boolean | `false` | 关闭自动创建 API 网关功能。默认值为否,即默认自动创建 API 网关。 | ##### 使用计划 -参考: https:# cloud.tencent.com/document/product/628/14947 +参考: https://cloud.tencent.com/document/product/628/14947 | 参数名称 | 是否必选 | 类型 | 描述 | | ------------- | :------: | ------ | :------------------------------------------------------ | | usagePlanId | 否 | String | 用户自定义使用计划 ID | | usagePlanName | 否 | String | 用户自定义的使用计划名称 | | usagePlanDesc | 否 | String | 用户自定义的使用计划描述 | -| maxRequestNum | 否 | Int | 请求配额总数,如果为空,将使用-1 作为默认值,表示不开启 | +| maxRequestNum | 否 | Number | 请求配额总数,如果为空,将使用-1 作为默认值,表示不开启 | ##### API 密钥配置 -参考: https:# cloud.tencent.com/document/product/628/14916 +参考: https://cloud.tencent.com/document/product/628/14916 | 参数名称 | 类型 | 描述 | | ---------- | :----- | :------- | @@ -179,15 +181,15 @@ inputs: ##### 自定义域名 -Refer to: https:# cloud.tencent.com/document/product/628/14906 +Refer to: https://cloud.tencent.com/document/product/628/14906 -| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | -| ---------------- | :------: | :------: | :------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| domain | 是 | String | | 待绑定的自定义的域名。 | -| certificateId | 否 | String | | 待绑定自定义域名的证书唯一 ID,如果设置了 type 为 https,则为必选 | -| isDefaultMapping | 否 | String | `'TRUE'` | 是否使用默认路径映射,默认为 TRUE。为 FALSE 时,表示自定义路径映射,此时 pathMappingSet 必填。 | -| pathMappingSet | 否 | Object[] | `[]` | 自定义路径映射的路径。使用自定义映射时,可一次仅映射一个 path 到一个环境,也可映射多个 path 到多个环境。并且一旦使用自定义映射,原本的默认映射规则不再生效,只有自定义映射路径生效。 | -| protocol | 否 | String[] | | 绑定自定义域名的协议类型,默认与服务的前端协议一致。 | +| 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | +| ---------------- | :------: | :------: | :----: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| domain | 是 | String | | 待绑定的自定义的域名。 | +| certificateId | 否 | String | | 待绑定自定义域名的证书唯一 ID,如果设置了 type 为 https,则为必选 | +| isDefaultMapping | 否 | String | `true` | 是否使用默认路径映射。为 false 时,表示自定义路径映射,此时 pathMappingSet 必填。 | +| pathMappingSet | 否 | Object[] | `[]` | 自定义路径映射的路径。使用自定义映射时,可一次仅映射一个 path 到一个环境,也可映射多个 path 到多个环境。并且一旦使用自定义映射,原本的默认映射规则不再生效,只有自定义映射路径生效。 | +| protocol | 否 | String[] | | 绑定自定义域名的协议类型,默认与服务的前端协议一致。 | - 自定义路径映射 diff --git a/example/.prettierrc b/example/.prettierrc new file mode 100644 index 0000000..dcb7279 --- /dev/null +++ b/example/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/example/README.md b/example/README.md index f9101bc..d85b505 100644 --- a/example/README.md +++ b/example/README.md @@ -1,75 +1,171 @@ -

- Nest Logo -

- -[travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master -[travis-url]: https://travis-ci.org/nestjs/nest -[linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux -[linux-url]: https://travis-ci.org/nestjs/nest - -

A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.

-

-NPM Version -Package License -NPM Downloads -Travis -Linux -Coverage -Gitter -Backers on Open Collective -Sponsors on Open Collective - - -

- - -## Description - -[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. - -## Installation +# Serverless Nest.js + +**腾讯云 Nest.js 组件** ⎯⎯⎯ 通过使用 [Serverless Framework](https://github.com/serverless/components/tree/cloud),基于云上 Serverless 服务(如网关、云函数等),实现“0”配置,便捷开发,极速部署你的 Nest.js 应用。 + +> 注意:本项目仅支持使用 [ExpressAdapter](https://docs.nestjs.com/faq/http-adapter) 的 Nest.js 项目。 + +### 安装 + +通过 npm 安装最新版本的 Serverless Framework ```bash -$ npm install +$ npm install -g serverless ``` -## Running the app +### 创建 + +通过如下命令和模板链接,快速创建一个 Nest.js 应用: ```bash -# development -$ npm run start +$ npm i -g @nestjs/cli +$ nest new serverless-nestjs +$ cd serverless-nestjs +``` + +执行如下命令,安装应用的对应依赖 + +``` +$ npm install +``` + +### 项目改造 + +1. 由于云端函数执行时不需要监听端口的,所以我们需要修改下入口文件 `src/main.ts`,如下: -# watch mode -$ npm run start:dev +```typescript +import { NestFactory } from '@nestjs/core'; +import { join } from 'path'; +import { AppModule } from './app.module'; -# production mode -$ npm run start:prod +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + return app; +} + +// TODO: 通过注入 NODE_ENV 为 local,来方便本地启动服务,进行开发调试 +const isLocal = process.env.NODE_ENV === 'local'; +if (isLocal) { + bootstrap().then(app => { + app.listen(3000, () => { + console.log(`Server start on http://localhost:3000`); + }); + }); +} + +// 导出启动函数,给 sls.js 使用 +export { bootstrap }; +``` + +2. 项目根目录下新增 `sls.js` 文件,用来提供给 Nest.js 组件使用: + +```js +// 注意: 根据实际项目入口文件名进行修改,此 demo 为默认情况 +const { bootstrap } = require('./dist/main'); + +module.exports = bootstrap; ``` -## Test +> 注意:实际开发可根据个人项目路径,来导出启动函数 `bootstrap`。 + +### 部署 + +由于 Nest.js 项目是 TypeScript,部署前需要编译成 JavaScript,在项目中执行编译命令即可: ```bash -# unit tests -$ npm run test +$ npm run build +``` + +在 `serverless.yml` 文件下的目录中运行 `serverless deploy` 进行 Nest.js 项目的部署。第一次部署可能耗时相对较久,但后续的二次部署会在几秒钟之内完成。部署完毕后,你可以在命令行的输出中查看到你 nestjs 应用的 URL 地址,点击地址即可访问你的 Nest.js 项目。 + +> **注意:** 如您的账号未 [登录](https://cloud.tencent.com/login) 或 [注册](https://cloud.tencent.com/register) 腾讯云,您可以直接通过 `微信` 扫描命令行中的二维码进行授权登陆和注册。 + +如果希望查看更多部署过程的信息,可以通过`sls deploy --debug` 命令查看部署过程中的实时日志信息,`sls`是 `serverless` 命令的缩写。 + +### 配置 -# e2e tests -$ npm run test:e2e +nestjs 组件支持 0 配置部署,也就是可以直接通过配置文件中的默认值进行部署。但你依然可以修改更多可选配置来进一步开发该 nestjs 项目。 + +以下是 nestjs 组件的 `serverless.yml` 简单配置示例: + +```yml +# serverless.yml + +component: nestjs # (required) name of the component. In that case, it's nestjs. +name: nestjsDemo # (required) name of your nestjs component instance. +org: orgDemo # (optional) serverless dashboard org. default is the first org you created during signup. +app: appDemo # (optional) serverless dashboard app. default is the same as the name property. +stage: dev # (optional) serverless dashboard stage. default is dev. + +inputs: + src: + src: ./ # (optional) path to the source folder. default is a hello world app. + exclude: + - .env + functionName: nestjsDemo + region: ap-guangzhou + runtime: Nodejs10.15 + apigatewayConf: + protocols: + - http + - https + environment: release +``` + +点此查看[全量配置及配置说明](https://github.com/serverless-components/tencent-nestjs/tree/master/docs/configure.md) + +当你根据该配置文件更新配置字段后,再次运行 `serverless deploy` 或者 `serverless` 就可以更新配置到云端。 + +### 远程调试云函数 + +部署了 nestjs.js 应用后,可以通过开发调试能力对该项目进行二次开发,从而开发一个生产应用。在本地修改和更新代码后,不需要每次都运行 `serverless deploy` 命令来反复部署。你可以直接通过 `serverless dev` 命令对本地代码的改动进行检测和自动上传。 + +可以通过在 `serverless.yml`文件所在的目录下运行 `serverless dev` 命令开启开发调试能力。 + +`serverless dev` 同时支持实时输出云端日志,每次部署完毕后,对项目进行访问,即可在命令行中实时输出调用日志,便于查看业务情况和排障。 + +除了实时日志输出之外,针对 Node.js 应用,当前也支持云端调试能力。在开启 `serverless dev` 命令之后,将会自动监听远端端口,并将函数的超时时间临时配置为 900s。此时你可以通过访问 chrome://inspect/#devices 查找远端的调试路径,并直接对云端代码进行断点等调试。在调试模式结束后,需要再次部署从而将代码更新并将超时时间设置为原来的值。详情参考[开发模式和云端调试](https://cloud.tencent.com/document/product/1154/43220)。 + +### 查看状态 + +在`serverless.yml`文件所在的目录下,通过如下命令查看部署状态: -# test coverage -$ npm run test:cov ``` +$ serverless info +``` + +### 移除 + +在`serverless.yml`文件所在的目录下,通过以下命令移除部署的 nestjs 服务。移除后该组件会对应删除云上部署时所创建的所有相关资源。 + +``` +$ serverless remove +``` + +和部署类似,支持通过 `sls remove --debug` 命令查看移除过程中的实时日志信息,`sls`是 `serverless` 命令的缩写。 -## Support +## 账号配置 -Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). +当前默认支持 CLI 扫描二维码登录,如您希望配置持久的环境变量/秘钥信息,也可以本地创建 `.env` 文件 -## Stay in touch +```console +$ touch .env # 腾讯云的配置信息 +``` + +在 `.env` 文件中配置腾讯云的 SecretId 和 SecretKey 信息并保存 -- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) -- Website - [https://nestjs.com](https://nestjs.com/) -- Twitter - [@nestframework](https://twitter.com/nestframework) +如果没有腾讯云账号,可以在此[注册新账号](https://cloud.tencent.com/register)。 + +如果已有腾讯云账号,可以在[API 密钥管理](https://console.cloud.tencent.com/cam/capi)中获取 `SecretId` 和`SecretKey`. + +``` +# .env +TENCENT_SECRET_ID=123 +TENCENT_SECRET_KEY=123 +``` ## License -Nest is [MIT licensed](LICENSE). +MIT License + +Copyright (c) 2020 Tencent Cloud, Inc. diff --git a/example/handler.js b/example/handler.js deleted file mode 100644 index 5812c23..0000000 --- a/example/handler.js +++ /dev/null @@ -1,26 +0,0 @@ -// require('tencent-component-monitor') -const fs = require('fs') -const path = require('path') -const { createServer, proxy } = require('tencent-serverless-http') - -let server -const createApp = require('./dist/main') - -exports.handler = async (event, context) => { - const app = await createApp() - // cache server, not create repeatly - if (!server) { - server = createServer(app, null, app.binaryTypes || []) - } - - context.callbackWaitsForEmptyEventLoop = - app.callbackWaitsForEmptyEventLoop === true ? true : false - - // provide sls intialize hooks - if (app.slsInitialize && typeof app.slsInitialize === 'function') { - await app.slsInitialize() - } - - const result = await proxy(server, event, context, 'PROMISE') - return result.promise -} diff --git a/example/package.json b/example/package.json index 89d68c1..26ee5bb 100644 --- a/example/package.json +++ b/example/package.json @@ -1,5 +1,5 @@ { - "name": "nest-demo", + "name": "nestjs-demo", "version": "0.0.1", "description": "", "author": "", @@ -9,16 +9,10 @@ "prebuild": "rimraf dist", "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "start": "NODE_ENV=local nest start", "start:dev": "NODE_ENV=local nest start --watch", - "start:debug": "NODE_ENV=local nest start --debug --watch", - "start:prod": "NODE_ENV=local node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "start:debug": "NODE_ENV=local nest start --debug --watch" }, "dependencies": { "@nestjs/common": "^7.0.0", @@ -27,28 +21,27 @@ "hbs": "^4.1.1", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", - "rxjs": "^6.5.4", - "tencent-serverless-http": "^1.2.0" + "rxjs": "^6.5.4" }, "devDependencies": { "@nestjs/cli": "^7.0.0", "@nestjs/schematics": "^7.0.0", "@nestjs/testing": "^7.0.0", "@types/express": "^4.17.3", - "@types/jest": "25.2.3", + "@types/jest": "26.0.10", "@types/node": "^13.9.1", "@types/supertest": "^2.0.8", - "@typescript-eslint/eslint-plugin": "3.0.2", - "@typescript-eslint/parser": "3.0.2", - "eslint": "7.1.0", + "@typescript-eslint/eslint-plugin": "3.9.1", + "@typescript-eslint/parser": "3.9.1", + "eslint": "7.7.0", "eslint-config-prettier": "^6.10.0", "eslint-plugin-import": "^2.20.1", - "jest": "26.0.1", + "jest": "26.4.2", "prettier": "^1.19.1", "supertest": "^4.0.2", - "ts-jest": "26.1.0", + "ts-jest": "26.2.0", "ts-loader": "^6.2.1", - "ts-node": "^8.6.2", + "ts-node": "9.0.0", "tsconfig-paths": "^3.9.0", "typescript": "^3.7.4" }, diff --git a/example/serverless.yml b/example/serverless.yml index 9eae45b..7509a44 100644 --- a/example/serverless.yml +++ b/example/serverless.yml @@ -1,18 +1,19 @@ -org: orgDemo # (optional) serverless dashboard org. default is the first org you created during signup. -app: appDemo # (optional) serverless dashboard app. default is the same as the name property. -stage: dev # (optional) serverless dashboard stage. default is dev. -component: express # (required) name of the component. In that case, it's express. -name: expressDemo # (required) name of your express component instance. +app: appDemo +org: orgDemo +stage: dev +component: nestjs +name: nestjsDemo inputs: src: - src: ./ # (optional) path to the source folder. default is a hello world app. + src: ./ exclude: - .env - region: ap-guangzhou - runtime: Nodejs10.15 - apigatewayConf: - protocols: - - http - - https - environment: release + - 'test/**' + # functionConf: + # timeout: 10 + # memorySize: 128 + # apigatewayConf: + # protocols: + # - http + # - https diff --git a/example/sls.js b/example/sls.js index 195d143..b5e8fc9 100644 --- a/example/sls.js +++ b/example/sls.js @@ -1,3 +1,4 @@ -const createServer = require('./dist/main') +// Notice: change filename to your real project entry filename +const { bootstrap } = require('./dist/main'); -module.exports = createServer +module.exports = bootstrap; diff --git a/example/src/main.ts b/example/src/main.ts index 6932a51..0af609a 100644 --- a/example/src/main.ts +++ b/example/src/main.ts @@ -1,41 +1,25 @@ import { NestFactory } from '@nestjs/core'; -import { - NestExpressApplication, - ExpressAdapter, -} from '@nestjs/platform-express'; +import { NestExpressApplication } from '@nestjs/platform-express'; import { join } from 'path'; -import * as express from 'express'; import { AppModule } from './app.module'; -// -const createServerlessApp = async (): Promise => { - const expressApp = express(); - - const adapter = new ExpressAdapter(expressApp); - const app = await NestFactory.create( - AppModule, - adapter, - ); - app.enableCors(); - app.setBaseViewsDir(join(__dirname, '..', 'views')); - app.setViewEngine('hbs'); - - await app.init(); - return app; -}; - async function bootstrap() { const app = await NestFactory.create(AppModule); - + app.enableCors(); app.setBaseViewsDir(join(__dirname, '..', 'views')); - app.setViewEngine('hbs'); - await app.listen(3000); + return app; } -if (process.env.NODE_ENV === 'local') { - bootstrap(); -} else { - module.exports = createServerlessApp; +// TODO: 通过注入 NODE_ENV 为 local,来方便本地启动服务,进行开发调试 +const isLocal = process.env.NODE_ENV === 'local'; +if (isLocal) { + bootstrap().then(app => { + app.listen(3000, () => { + console.log(`Server start on http://localhost:3000`); + }); + }); } + +export { bootstrap }; diff --git a/package.json b/package.json index a03aea4..9e32309 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "@serverless/nestjs", - "version": "0.0.0", "main": "src/serverless.js", "publishConfig": { "access": "public" @@ -10,17 +9,17 @@ "commitlint": "commitlint -f HEAD@{15}", "lint": "eslint --ext .js,.ts,.tsx .", "lint:fix": "eslint --fix --ext .js,.ts,.tsx .", - "prettier": "prettier --check **/*.{css,html,js,json,md,yaml,yml}", - "prettier:fix": "prettier --write **/*.{css,html,js,json,md,yaml,yml}", + "prettier": "prettier --check '**/*.{css,html,js,json,md,yaml,yml}'", + "prettier:fix": "prettier --write '**/*.{css,html,js,json,md,yaml,yml}'", "release": "semantic-release", "release-local": "node -r dotenv/config node_modules/semantic-release/bin/semantic-release --no-ci --dry-run", "check-dependencies": "npx npm-check --skip-unused --update" }, "husky": { "hooks": { - "pre-commit": "lint-staged", + "pre-commit": "ygsec && lint-staged", "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", - "pre-push": "npm run lint:fix && npm run prettier:fix" + "pre-push": "ygsec && npm run lint:fix && npm run prettier:fix" } }, "lint-staged": { @@ -44,13 +43,17 @@ "@semantic-release/git": "^9.0.0", "@semantic-release/npm": "^7.0.4", "@semantic-release/release-notes-generator": "^9.0.1", + "@serverless/platform-client-china": "^1.0.19", + "@ygkit/secure": "0.0.3", + "axios": "^0.19.2", "babel-eslint": "^10.1.0", "dotenv": "^8.2.0", "eslint": "^6.8.0", "eslint-config-prettier": "^6.10.0", "eslint-plugin-import": "^2.20.1", "eslint-plugin-prettier": "^3.1.2", - "husky": "^4.2.3", + "husky": "^4.2.5", + "jest": "^25.0.1", "lint-staged": "^10.0.8", "prettier": "^1.19.1", "semantic-release": "^17.0.4" diff --git a/release.config.js b/release.config.js index 53f3398..98b3864 100644 --- a/release.config.js +++ b/release.config.js @@ -1,7 +1,6 @@ module.exports = { verifyConditions: [ '@semantic-release/changelog', - '@semantic-release/npm', '@semantic-release/git', '@semantic-release/github' ], @@ -33,14 +32,6 @@ module.exports = { changelogFile: 'CHANGELOG.md' } ], - [ - '@semantic-release/npm', - { - pkgRoot: '.', - npmPublish: false, - tarballDir: false - } - ], [ '@semantic-release/git', { diff --git a/serverless.component.yml b/serverless.component.yml index 74796e5..b11a675 100644 --- a/serverless.component.yml +++ b/serverless.component.yml @@ -1,10 +1,11 @@ name: nestjs -version: dev +version: 0.1.0 author: Tencent Cloud, Inc. org: Tencent Cloud, Inc. -description: Deploys a serverless Nest.js application onto Tencent SCF and Tencent APIGateway. +description: Deploy a serverless Nest.js application onto Tencent SCF and APIGateway. keywords: tencent, serverless, nestjs repo: https://github.com/serverless-components/tencent-nestjs -readme: https://github.com/serverless-components/tencent-nestjs/README.md +readme: https://github.com/serverless-components/tencent-nestjs/tree/master/README.md license: MIT main: ./src +webDeployable: true diff --git a/src/_shims/handler.js b/src/_shims/handler.js index 664fba6..b8dc26a 100644 --- a/src/_shims/handler.js +++ b/src/_shims/handler.js @@ -1,20 +1,14 @@ -require('tencent-component-monitor') -const fs = require('fs') const path = require('path') const { createServer, proxy } = require('tencent-serverless-http') +const userSls = path.join(__dirname, '..', 'sls.js') +const getApp = require(userSls) let server exports.handler = async (event, context) => { - const userSls = path.join(__dirname, '..', 'sls.js') - let app - if (fs.existsSync(userSls)) { - // load the user provided app - app = require(userSls) - } else { - // load the built-in default app - app = require('./sls.js') - } + const nestApp = await getApp() + await nestApp.init() + const app = nestApp.getHttpAdapter().getInstance() // attach event and context to request app.request.__SLS_EVENT__ = event @@ -25,14 +19,13 @@ exports.handler = async (event, context) => { server = createServer(app, null, app.binaryTypes || []) } - context.callbackWaitsForEmptyEventLoop = - app.callbackWaitsForEmptyEventLoop === true ? true : false + context.callbackWaitsForEmptyEventLoop = app.callbackWaitsForEmptyEventLoop === true // provide sls intialize hooks if (app.slsInitialize && typeof app.slsInitialize === 'function') { await app.slsInitialize() } - const result = await proxy(server, event, context, 'PROMISE') - return result.promise + const { promise } = await proxy(server, event, context, 'PROMISE') + return promise } diff --git a/src/_shims/package.json b/src/_shims/package.json index 2140c5d..680a1e7 100644 --- a/src/_shims/package.json +++ b/src/_shims/package.json @@ -1,17 +1,5 @@ { - "name": "@serverless/express", - "main": "./handler.js", - "publishConfig": { - "access": "public" - }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "lint": "eslint . --fix --cache" - }, - "author": "Serverless, Inc.", - "license": "Apache", "dependencies": { - "tencent-component-monitor": "^1.1.0", "tencent-serverless-http": "^1.2.0" } } diff --git a/src/_shims/sls.js b/src/_shims/sls.js deleted file mode 100644 index 92478ff..0000000 --- a/src/_shims/sls.js +++ /dev/null @@ -1,18 +0,0 @@ -const express = require('express') -const app = express() - -// Routes -app.get(`/`, (req, res) => { - res.send({ - msg: `Hello Express, Request received: ${req.method} - ${req.path}` - }) -}) - -// Error handler -// eslint-disable-next-line -app.use(function(err, req, res, next) { - console.error(err) - res.status(500).send('Internal Serverless Error') -}) - -module.exports = app diff --git a/src/config.js b/src/config.js index 61cbdc7..c7df09f 100644 --- a/src/config.js +++ b/src/config.js @@ -1,15 +1,14 @@ const CONFIGS = { templateUrl: - 'https://serverless-templates-1300862921.cos.ap-beijing.myqcloud.com/express-demo.zip', - framework: 'express', - frameworkFullname: 'Express.js', + 'https://serverless-templates-1300862921.cos.ap-beijing.myqcloud.com/nestjs-demo.zip', + compName: 'nestjs', + compFullname: 'Nest.js', handler: 'sl_handler.handler', runtime: 'Nodejs10.15', - exclude: ['.git/**', '.gitignore', '.DS_Store'], timeout: 3, memorySize: 128, namespace: 'default', - description: 'This is a function created by serverless component' + description: 'Created by Serverless Component' } module.exports = CONFIGS diff --git a/src/package.json b/src/package.json index 06ff0d5..4e09522 100644 --- a/src/package.json +++ b/src/package.json @@ -1,18 +1,7 @@ { - "name": "@serverless/nestjs", - "main": "./serverless.js", - "publishConfig": { - "access": "public" - }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "lint": "eslint . --fix --cache" - }, - "author": "Tencent Cloud, Inc.", - "license": "MIT", "dependencies": { "download": "^8.0.0", - "tencent-component-toolkit": "^1.10.1", - "type": "^2.0.0" + "tencent-component-toolkit": "^1.16.4", + "type": "^2.1.0" } } diff --git a/src/serverless.js b/src/serverless.js index 0a918e1..86454e0 100644 --- a/src/serverless.js +++ b/src/serverless.js @@ -1,17 +1,17 @@ const { Component } = require('@serverless/core') -const { MultiApigw, Scf, Apigw, Cns, Cam, Metrics } = require('tencent-component-toolkit') +const { Scf, Apigw, Cns, Cam, Metrics } = require('tencent-component-toolkit') const { TypeError } = require('tencent-component-toolkit/src/utils/error') -const { uploadCodeToCos, getDefaultProtocol, deleteRecord, prepareInputs } = require('./utils') +const { deepClone, uploadCodeToCos, getDefaultProtocol, prepareInputs } = require('./utils') const CONFIGS = require('./config') -class ServerlessComponent extends Component { +class ServerlessComopnent extends Component { getCredentials() { const { tmpSecrets } = this.credentials.tencent if (!tmpSecrets || !tmpSecrets.TmpSecretId) { throw new TypeError( 'CREDENTIAL', - 'Cannot get secretId/Key, your account could be sub-account or does not have access, please check if SLS_QcsRole role exists in your account, and visit https://console.cloud.tencent.com/cam to bind this role to your account.' + 'Cannot get secretId/Key, your account could be sub-account and does not have the access to use SLS_QcsRole, please make sure the role exists first, then visit https://cloud.tencent.com/document/product/1154/43006, follow the instructions to bind the role to your account.' ) } @@ -39,125 +39,152 @@ class ServerlessComponent extends Component { } } - const uploadCodeHandler = [] const outputs = {} const appId = this.getAppId() - for (let eveRegionIndex = 0; eveRegionIndex < regionList.length; eveRegionIndex++) { - const curRegion = regionList[eveRegionIndex] - const funcDeployer = async () => { - const code = await uploadCodeToCos(this, appId, credentials, inputs, curRegion) - const scf = new Scf(credentials, curRegion) - const tempInputs = { - ...inputs, - code - } - const scfOutput = await scf.deploy(tempInputs) - outputs[curRegion] = { - functionName: scfOutput.FunctionName, - runtime: scfOutput.Runtime, - namespace: scfOutput.Namespace - } + const funcDeployer = async (curRegion) => { + const code = await uploadCodeToCos(this, appId, credentials, inputs, curRegion) + const scf = new Scf(credentials, curRegion) + const tempInputs = { + ...inputs, + code + } + const scfOutput = await scf.deploy(deepClone(tempInputs)) + outputs[curRegion] = { + functionName: scfOutput.FunctionName, + runtime: scfOutput.Runtime, + namespace: scfOutput.Namespace + } - this.state[curRegion] = { - ...(this.state[curRegion] ? this.state[curRegion] : {}), - ...outputs[curRegion] - } + this.state[curRegion] = { + ...(this.state[curRegion] ? this.state[curRegion] : {}), + ...outputs[curRegion] } - uploadCodeHandler.push(funcDeployer()) + + // default version is $LATEST + outputs[curRegion].lastVersion = scfOutput.LastVersion + ? scfOutput.LastVersion + : this.state.lastVersion || '$LATEST' + + // default traffic is 1.0, it can also be 0, so we should compare to undefined + outputs[curRegion].traffic = + scfOutput.Traffic !== undefined + ? scfOutput.Traffic + : this.state.traffic !== undefined + ? this.state.traffic + : 1 + + if (outputs[curRegion].traffic !== 1 && scfOutput.ConfigTrafficVersion) { + outputs[curRegion].configTrafficVersion = scfOutput.ConfigTrafficVersion + this.state.configTrafficVersion = scfOutput.ConfigTrafficVersion + } + + this.state.lastVersion = outputs[curRegion].lastVersion + this.state.traffic = outputs[curRegion].traffic + } + + for (let i = 0; i < regionList.length; i++) { + const curRegion = regionList[i] + await funcDeployer(curRegion) } - await Promise.all(uploadCodeHandler) this.save() return outputs } + // try to add dns record + async tryToAddDnsRecord(credentials, customDomains) { + try { + const cns = new Cns(credentials) + for (let i = 0; i < customDomains.length; i++) { + const item = customDomains[i] + if (item.domainPrefix) { + await cns.deploy({ + domain: item.subDomain.replace(`${item.domainPrefix}.`, ''), + records: [ + { + subDomain: item.domainPrefix, + recordType: 'CNAME', + recordLine: '默认', + value: item.cname, + ttl: 600, + mx: 10, + status: 'enable' + } + ] + }) + } + } + } catch (e) { + console.log('METHOD_tryToAddDnsRecord', e.message) + } + } + async deployApigateway(credentials, inputs, regionList) { if (inputs.isDisabled) { return {} } - const apigw = new MultiApigw(credentials, regionList) - const oldState = this.state[regionList[0]] || {} - inputs.oldState = { - apiList: oldState.apiList || [], - customDomains: oldState.customDomains || [] + + const getServiceId = (instance, region) => { + const regionState = instance.state[region] + return inputs.serviceId || (regionState && regionState.serviceId) } - const apigwOutputs = await apigw.deploy(inputs) - const outputs = {} - Object.keys(apigwOutputs).forEach((curRegion) => { - const curOutput = apigwOutputs[curRegion] - outputs[curRegion] = { - serviceId: curOutput.serviceId, - subDomain: curOutput.subDomain, - environment: curOutput.environment, - url: `${getDefaultProtocol(inputs.protocols)}://${curOutput.subDomain}/${ - curOutput.environment - }/` - } - if (curOutput.customDomains) { - outputs[curRegion].customDomains = curOutput.customDomains - } - this.state[curRegion] = { - created: curOutput.created, - ...(this.state[curRegion] ? this.state[curRegion] : {}), - ...outputs[curRegion], - apiList: curOutput.apiList - } - }) - this.save() - return outputs - } - async deployCns(credentials, inputs, regionList, apigwOutputs) { - const cns = new Cns(credentials) - const cnsRegion = {} + const deployTasks = [] + const outputs = {} regionList.forEach((curRegion) => { - const curApigwOutput = apigwOutputs[curRegion] - cnsRegion[curRegion] = curApigwOutput.subDomain - }) + const apigwDeployer = async () => { + const apigw = new Apigw(credentials, curRegion) - const state = [] - const outputs = {} - const tempJson = {} - for (let i = 0; i < inputs.length; i++) { - const curCns = inputs[i] - for (let j = 0; j < curCns.records.length; j++) { - curCns.records[j].value = - cnsRegion[curCns.records[j].value.replace('temp_value_about_', '')] - } - const tencentCnsOutputs = await cns.deploy(curCns) - outputs[curCns.domain] = tencentCnsOutputs.DNS - ? tencentCnsOutputs.DNS - : 'The domain name has already been added.' - tencentCnsOutputs.domain = curCns.domain - state.push(tencentCnsOutputs) - } + const oldState = this.state[curRegion] || {} + const apigwInputs = { + ...inputs, + oldState: { + apiList: oldState.apiList || [], + customDomains: oldState.customDomains || [] + } + } + // different region deployment has different service id + apigwInputs.serviceId = getServiceId(this, curRegion) + const apigwOutput = await apigw.deploy(deepClone(apigwInputs)) + outputs[curRegion] = { + serviceId: apigwOutput.serviceId, + subDomain: apigwOutput.subDomain, + environment: apigwOutput.environment, + url: `${getDefaultProtocol(inputs.protocols)}://${apigwOutput.subDomain}/${ + apigwOutput.environment + }/` + } - // 删除serverless创建的但是不在本次列表中 - try { - for (let i = 0; i < state.length; i++) { - tempJson[state[i].domain] = state[i].records - } - const recordHistory = this.state.cns || [] - for (let i = 0; i < recordHistory.length; i++) { - const delList = deleteRecord(tempJson[recordHistory[i].domain], recordHistory[i].records) - if (delList && delList.length > 0) { - await cns.remove({ deleteList: delList }) + if (apigwOutput.customDomains) { + // TODO: need confirm add cns authentication + if (inputs.autoAddDnsRecord === true) { + // await this.tryToAddDnsRecord(credentials, apigwOutput.customDomains) + } + outputs[curRegion].customDomains = apigwOutput.customDomains + } + this.state[curRegion] = { + created: true, + ...(this.state[curRegion] ? this.state[curRegion] : {}), + ...outputs[curRegion], + apiList: apigwOutput.apiList } } - } catch (e) {} + deployTasks.push(apigwDeployer()) + }) + + await Promise.all(deployTasks) - this.state['cns'] = state this.save() return outputs } async deploy(inputs) { - console.log(`Deploying ${CONFIGS.frameworkFullname} App...`) + console.log(`Deploying ${CONFIGS.compFullname} App`) const credentials = this.getCredentials() // 对Inputs内容进行标准化 - const { regionList, functionConf, apigatewayConf, cnsConf } = await prepareInputs( + const { regionList, functionConf, apigatewayConf } = await prepareInputs( this, credentials, inputs @@ -189,20 +216,16 @@ class ServerlessComponent extends Component { outputs['scf'] = functionOutputs } - // cns depends on apigw, so if disabled apigw, just ignore it. - if (cnsConf.length > 0 && apigatewayConf.isDisabled !== true) { - outputs['cns'] = await this.deployCns(credentials, cnsConf, regionList, apigwOutputs) - } - this.state.region = regionList[0] this.state.regionList = regionList + this.state.lambdaArn = functionConf.name return outputs } async remove() { - console.log(`Removing ${CONFIGS.frameworkFullname} App...`) + console.log(`Removing ${CONFIGS.compFullname} App`) const { state } = this const { regionList = [] } = state @@ -236,24 +259,23 @@ class ServerlessComponent extends Component { await Promise.all(removeHandlers) - if (this.state.cns) { - const cns = new Cns(credentials) - for (let i = 0; i < this.state.cns.length; i++) { - await cns.remove({ deleteList: this.state.cns[i].records }) - } - } - this.state = {} } async metrics(inputs = {}) { - console.log(`Get ${CONFIGS.frameworkFullname} Metrics Datas...`) + console.log(`Get ${CONFIGS.compFullname} Metrics Datas`) if (!inputs.rangeStart || !inputs.rangeEnd) { - throw new TypeError('PARAMETER_METRICS', 'rangeStart and rangeEnd are require inputs') + throw new TypeError( + `PARAMETER_${CONFIGS.compName.toUpperCase()}_METRICS`, + 'rangeStart and rangeEnd are require inputs' + ) } const { region } = this.state if (!region) { - throw new TypeError('PARAMETER_METRICS', 'No region property in state') + throw new TypeError( + `PARAMETER_${CONFIGS.compName.toUpperCase()}_METRICS`, + 'No region property in state' + ) } const { functionName, namespace, functionVersion } = this.state[region] || {} if (functionName) { @@ -264,6 +286,11 @@ class ServerlessComponent extends Component { region, timezone: inputs.tz } + const curState = this.state[region] + if (curState.serviceId) { + options.apigwServiceId = curState.serviceId + options.apigwEnvironment = curState.environment || 'release' + } const credentials = this.getCredentials() const mertics = new Metrics(credentials, options) const metricResults = await mertics.getDatas( @@ -273,8 +300,11 @@ class ServerlessComponent extends Component { ) return metricResults } - throw new Error('function name not define') + throw new TypeError( + `PARAMETER_${CONFIGS.compName.toUpperCase()}_METRICS`, + 'Function name not define' + ) } } -module.exports = ServerlessComponent +module.exports = ServerlessComopnent diff --git a/src/utils.js b/src/utils.js index 409ce44..418f74d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,29 +1,74 @@ const path = require('path') -const { Domain, Cos } = require('tencent-component-toolkit') +const { Cos } = require('tencent-component-toolkit') +const { TypeError } = require('tencent-component-toolkit/src/utils/error') const ensureObject = require('type/object/ensure') const ensureIterable = require('type/iterable/ensure') const ensureString = require('type/string/ensure') const download = require('download') -const { TypeError } = require('tencent-component-toolkit/src/utils/error') const CONFIGS = require('./config') -/* - * Pauses execution for the provided miliseconds - * - * @param ${number} wait - number of miliseconds to wait - */ -const sleep = async (wait) => new Promise((resolve) => setTimeout(() => resolve(), wait)) - -/* - * Generates a random id - */ const generateId = () => Math.random() .toString(36) .substring(6) +const deepClone = (obj) => { + return JSON.parse(JSON.stringify(obj)) +} + +const getType = (obj) => { + return Object.prototype.toString.call(obj).slice(8, -1) +} + +const mergeJson = (sourceJson, targetJson) => { + Object.entries(sourceJson).forEach(([key, val]) => { + targetJson[key] = deepClone(val) + }) + return targetJson +} + +const capitalString = (str) => { + if (str.length < 2) { + return str.toUpperCase() + } + + return `${str[0].toUpperCase()}${str.slice(1)}` +} + +const getDefaultProtocol = (protocols) => { + return String(protocols).includes('https') ? 'https' : 'http' +} + +const getDefaultFunctionName = () => { + return `${CONFIGS.compName}_component_${generateId()}` +} + +const getDefaultServiceName = () => { + return 'serverless' +} + +const getDefaultServiceDescription = () => { + return 'Created by Serverless Component' +} + +const validateTraffic = (num) => { + if (getType(num) !== 'Number') { + throw new TypeError( + `PARAMETER_${CONFIGS.compName.toUpperCase()}_TRAFFIC`, + 'traffic must be a number' + ) + } + if (num < 0 || num > 1) { + throw new TypeError( + `PARAMETER_${CONFIGS.compName.toUpperCase()}_TRAFFIC`, + 'traffic must be a number between 0 and 1' + ) + } + return true +} + const getCodeZipPath = async (instance, inputs) => { - console.log(`Packaging ${CONFIGS.frameworkFullname} application...`) + console.log(`Packaging ${CONFIGS.compFullname} application...`) // unzip source zip file let zipPath @@ -32,7 +77,7 @@ const getCodeZipPath = async (instance, inputs) => { const downloadPath = `/tmp/${generateId()}` const filename = 'template' - console.log(`Installing Default ${CONFIGS.frameworkFullname} App...`) + console.log(`Installing Default ${CONFIGS.compFullname} App...`) try { await download(CONFIGS.templateUrl, downloadPath, { filename: `${filename}.zip` @@ -94,12 +139,18 @@ const uploadCodeToCos = async (instance, appId, credentials, inputs, region) => object: objectName, method: 'PUT' }) - const slsSDKEntries = instance.getSDKEntries('_shims/handler.handler') + // if shims and sls sdk entries had been injected to zipPath, no need to injected again console.log(`Uploading code to bucket ${bucketName}`) - await instance.uploadSourceZipToCOS(zipPath, uploadUrl, slsSDKEntries, { - _shims: path.join(__dirname, '_shims') - }) + if (instance.codeInjected === true) { + await instance.uploadSourceZipToCOS(zipPath, uploadUrl, {}, {}) + } else { + const slsSDKEntries = instance.getSDKEntries('_shims/handler.handler') + await instance.uploadSourceZipToCOS(zipPath, uploadUrl, slsSDKEntries, { + _shims: path.join(__dirname, '_shims') + }) + instance.codeInjected = true + } console.log(`Upload ${objectName} to bucket ${bucketName} success`) } } @@ -114,73 +165,10 @@ const uploadCodeToCos = async (instance, appId, credentials, inputs, region) => } } -const mergeJson = (sourceJson, targetJson) => { - for (const eveKey in sourceJson) { - if (targetJson.hasOwnProperty(eveKey)) { - if (['protocols', 'endpoints', 'customDomain'].indexOf(eveKey) != -1) { - for (let i = 0; i < sourceJson[eveKey].length; i++) { - const sourceEvents = JSON.stringify(sourceJson[eveKey][i]) - const targetEvents = JSON.stringify(targetJson[eveKey]) - if (targetEvents.indexOf(sourceEvents) == -1) { - targetJson[eveKey].push(sourceJson[eveKey][i]) - } - } - } else { - if (typeof sourceJson[eveKey] != 'string') { - mergeJson(sourceJson[eveKey], targetJson[eveKey]) - } else { - targetJson[eveKey] = sourceJson[eveKey] - } - } - } else { - targetJson[eveKey] = sourceJson[eveKey] - } - } - return targetJson -} - -const capitalString = (str) => { - if (str.length < 2) { - return str.toUpperCase() - } - - return `${str[0].toUpperCase()}${str.slice(1)}` -} - -const getDefaultProtocol = (protocols) => { - if (protocols.map((i) => i.toLowerCase()).includes('https')) { - return 'https' - } - return 'http' -} - -const deleteRecord = (newRecords, historyRcords) => { - const deleteList = [] - for (let i = 0; i < historyRcords.length; i++) { - let temp = false - for (let j = 0; j < newRecords.length; j++) { - if ( - newRecords[j].domain == historyRcords[i].domain && - newRecords[j].subDomain == historyRcords[i].subDomain && - newRecords[j].recordType == historyRcords[i].recordType && - newRecords[j].value == historyRcords[i].value && - newRecords[j].recordLine == historyRcords[i].recordLine - ) { - temp = true - break - } - } - if (!temp) { - deleteList.push(historyRcords[i]) - } - } - return deleteList -} - const prepareInputs = async (instance, credentials, inputs = {}) => { // 对function inputs进行标准化 const tempFunctionConf = inputs.functionConf ? inputs.functionConf : {} - const fromClientRemark = `tencent-${CONFIGS.framework}` + const fromClientRemark = `tencent-${CONFIGS.compName}` const regionList = inputs.region ? typeof inputs.region == 'string' ? [inputs.region] @@ -190,9 +178,6 @@ const prepareInputs = async (instance, credentials, inputs = {}) => { // chenck state function name const stateFunctionName = instance.state[regionList[0]] && instance.state[regionList[0]].functionName - // check state service id - const stateServiceId = instance.state[regionList[0]] && instance.state[regionList[0]].serviceId - const functionConf = { code: { src: inputs.src, @@ -202,7 +187,7 @@ const prepareInputs = async (instance, credentials, inputs = {}) => { name: ensureString(inputs.functionName, { isOptional: true }) || stateFunctionName || - `${CONFIGS.framework}_component_${generateId()}`, + getDefaultFunctionName(), region: regionList, role: ensureString(tempFunctionConf.role ? tempFunctionConf.role : inputs.role, { default: '' @@ -226,68 +211,77 @@ const prepareInputs = async (instance, credentials, inputs = {}) => { fromClientRemark, layers: ensureIterable(tempFunctionConf.layers ? tempFunctionConf.layers : inputs.layers, { default: [] + }), + cfs: ensureIterable(tempFunctionConf.cfs ? tempFunctionConf.cfs : inputs.cfs, { + default: [] + }), + publish: inputs.publish, + traffic: inputs.traffic, + lastVersion: instance.state.lastVersion, + eip: tempFunctionConf.eip === true, + l5Enable: tempFunctionConf.l5Enable === true, + timeout: tempFunctionConf.timeout ? tempFunctionConf.timeout : CONFIGS.timeout, + memorySize: tempFunctionConf.memorySize ? tempFunctionConf.memorySize : CONFIGS.memorySize, + tags: ensureObject(tempFunctionConf.tags ? tempFunctionConf.tags : inputs.tag, { + default: null }) } - functionConf.tags = ensureObject(tempFunctionConf.tags ? tempFunctionConf.tags : inputs.tag, { - default: null - }) - functionConf.include = ensureIterable( - tempFunctionConf.include ? tempFunctionConf.include : inputs.include, - { default: [], ensureItem: ensureString } - ) - functionConf.exclude = ensureIterable( - tempFunctionConf.exclude ? tempFunctionConf.exclude : inputs.exclude, - { default: [], ensureItem: ensureString } - ) - functionConf.exclude.push('.git/**', '.gitignore', '.serverless', '.DS_Store') - if (inputs.functionConf) { - functionConf.timeout = inputs.functionConf.timeout - ? inputs.functionConf.timeout - : CONFIGS.timeout - functionConf.memorySize = inputs.functionConf.memorySize - ? inputs.functionConf.memorySize - : CONFIGS.memorySize - if (inputs.functionConf.environment) { - functionConf.environment = inputs.functionConf.environment - } - if (inputs.functionConf.vpcConfig) { - functionConf.vpcConfig = inputs.functionConf.vpcConfig - } + // validate traffic + if (inputs.traffic !== undefined) { + validateTraffic(inputs.traffic) + } + functionConf.needSetTraffic = inputs.traffic !== undefined && functionConf.lastVersion + + if (tempFunctionConf.environment) { + functionConf.environment = inputs.functionConf.environment + } + if (tempFunctionConf.vpcConfig) { + functionConf.vpcConfig = inputs.functionConf.vpcConfig } // 对apigw inputs进行标准化 - const apigatewayConf = inputs.apigatewayConf ? inputs.apigatewayConf : {} - apigatewayConf.fromClientRemark = fromClientRemark - apigatewayConf.serviceName = inputs.serviceName - apigatewayConf.description = `Serverless Framework Tencent-${capitalString( - CONFIGS.framework - )} Component` - apigatewayConf.serviceId = inputs.serviceId || stateServiceId - apigatewayConf.region = functionConf.region - apigatewayConf.protocols = apigatewayConf.protocols || ['http'] - apigatewayConf.environment = apigatewayConf.environment ? apigatewayConf.environment : 'release' - apigatewayConf.endpoints = [ - { - path: '/', - enableCORS: apigatewayConf.enableCORS, - serviceTimeout: apigatewayConf.serviceTimeout, - method: 'ANY', - function: { - isIntegratedResponse: apigatewayConf.isIntegratedResponse === false ? false : true, - functionName: functionConf.name, - functionNamespace: functionConf.namespace + const tempApigwConf = inputs.apigatewayConf ? inputs.apigatewayConf : {} + const apigatewayConf = { + serviceId: inputs.serviceId, + region: regionList, + isDisabled: tempApigwConf.isDisabled === true, + fromClientRemark: fromClientRemark, + serviceName: inputs.serviceName || getDefaultServiceName(instance), + description: getDefaultServiceDescription(instance), + protocols: tempApigwConf.protocols || ['http'], + environment: tempApigwConf.environment ? tempApigwConf.environment : 'release', + endpoints: [ + { + path: '/', + enableCORS: tempApigwConf.enableCORS, + serviceTimeout: tempApigwConf.serviceTimeout, + method: 'ANY', + function: { + isIntegratedResponse: true, + functionName: functionConf.name, + functionNamespace: functionConf.namespace + } } + ], + customDomains: tempApigwConf.customDomains || [] + } + if (tempApigwConf.usagePlan) { + apigatewayConf.endpoints[0].usagePlan = { + usagePlanId: tempApigwConf.usagePlan.usagePlanId, + usagePlanName: tempApigwConf.usagePlan.usagePlanName, + usagePlanDesc: tempApigwConf.usagePlan.usagePlanDesc, + maxRequestNum: tempApigwConf.usagePlan.maxRequestNum } - ] - - // 对cns inputs进行标准化 - const tempCnsConf = {} - const tempCnsBaseConf = inputs.cloudDNSConf ? inputs.cloudDNSConf : {} + } + if (tempApigwConf.auth) { + apigatewayConf.endpoints[0].auth = { + secretName: tempApigwConf.auth.secretName, + secretIds: tempApigwConf.auth.secretIds + } + } - // 分地域处理functionConf/apigatewayConf/cnsConf - for (let i = 0; i < functionConf.region.length; i++) { - const curRegion = functionConf.region[i] + regionList.forEach((curRegion) => { const curRegionConf = inputs[curRegion] if (curRegionConf && curRegionConf.functionConf) { functionConf[curRegion] = curRegionConf.functionConf @@ -295,63 +289,21 @@ const prepareInputs = async (instance, credentials, inputs = {}) => { if (curRegionConf && curRegionConf.apigatewayConf) { apigatewayConf[curRegion] = curRegionConf.apigatewayConf } - - const tempRegionCnsConf = mergeJson( - tempCnsBaseConf, - curRegionConf && curRegionConf.cloudDNSConf ? curRegionConf.cloudDNSConf : {} - ) - - tempCnsConf[functionConf.region[i]] = { - recordType: 'CNAME', - recordLine: tempRegionCnsConf.recordLine ? tempRegionCnsConf.recordLine : undefined, - ttl: tempRegionCnsConf.ttl, - mx: tempRegionCnsConf.mx, - status: tempRegionCnsConf.status ? tempRegionCnsConf.status : 'enable' - } - } - - const cnsConf = [] - // 对cns inputs进行检查和赋值 - if (apigatewayConf.customDomain && apigatewayConf.customDomain.length > 0) { - const domain = new Domain(credentials) - for (let domianNum = 0; domianNum < apigatewayConf.customDomain.length; domianNum++) { - const domainData = await domain.check(apigatewayConf.customDomain[domianNum].domain) - const tempInputs = { - domain: domainData.domain, - records: [] - } - for (let eveRecordNum = 0; eveRecordNum < functionConf.region.length; eveRecordNum++) { - if (tempCnsConf[functionConf.region[eveRecordNum]].recordLine) { - tempInputs.records.push({ - subDomain: domainData.subDomain || '@', - recordType: 'CNAME', - recordLine: tempCnsConf[functionConf.region[eveRecordNum]].recordLine, - value: `temp_value_about_${functionConf.region[eveRecordNum]}`, - ttl: tempCnsConf[functionConf.region[eveRecordNum]].ttl, - mx: tempCnsConf[functionConf.region[eveRecordNum]].mx, - status: tempCnsConf[functionConf.region[eveRecordNum]].status || 'enable' - }) - } - } - cnsConf.push(tempInputs) - } - } + }) return { regionList, functionConf, - apigatewayConf, - cnsConf + apigatewayConf } } module.exports = { + deepClone, generateId, - sleep, - uploadCodeToCos, mergeJson, capitalString, getDefaultProtocol, - deleteRecord, + uploadCodeToCos, prepareInputs }