├── .babelrc ├── .editorconfig ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── .yarnrc ├── LICENSE ├── README.md ├── global.d.ts ├── jest.config.js ├── lerna.json ├── package.json ├── packages ├── capi-web │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── capi │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ │ └── index.test.ts │ ├── package.json │ ├── src │ │ ├── factory.ts │ │ ├── index.ts │ │ └── utils.ts │ └── tsconfig.json ├── cls │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ │ └── index.test.ts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── typings │ │ │ └── index.ts │ │ └── utils.ts │ └── tsconfig.json ├── common │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ │ └── index.test.ts │ ├── package.json │ ├── src │ │ ├── cropto.ts │ │ ├── index.ts │ │ └── logger.ts │ └── tsconfig.json ├── faas │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ │ └── index.test.ts │ ├── package.json │ ├── src │ │ ├── apis.ts │ │ ├── constants.ts │ │ ├── dayjs.ts │ │ ├── index.ts │ │ ├── monitor │ │ │ ├── apis.ts │ │ │ └── index.ts │ │ ├── typings │ │ │ └── index.ts │ │ └── utils.ts │ └── tsconfig.json └── login │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ └── index.test.ts │ ├── package.json │ ├── src │ ├── constant.ts │ └── index.ts │ └── tsconfig.json ├── scripts ├── bundle.ts ├── logger.ts ├── project.ts ├── publish.ts └── tsconfig.json ├── tsconfig.base.json ├── tsconfig.json └── tslint.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "browsers": [ 9 | ">0.25%" 10 | ] 11 | }, 12 | "useBuiltIns": "usage" 13 | } 14 | ], 15 | "@babel/typescript" 16 | ], 17 | "env": { 18 | "development": { 19 | "plugins": [ 20 | "@babel/transform-runtime" 21 | ] 22 | }, 23 | "production": { 24 | "plugins": [ 25 | "@babel/transform-runtime", 26 | "transform-remove-console" 27 | ] 28 | }, 29 | "test": { 30 | "presets": [ 31 | [ 32 | "@babel/env", 33 | { 34 | "modules": "commonjs", 35 | "targets": { 36 | "node": "current" 37 | } 38 | } 39 | ], 40 | "@babel/typescript" 41 | ] 42 | } 43 | }, 44 | "plugins": [ 45 | "@babel/plugin-syntax-dynamic-import", 46 | "@babel/plugin-syntax-import-meta", 47 | "@babel/plugin-proposal-class-properties", 48 | "@babel/plugin-proposal-json-strings", 49 | [ 50 | "@babel/plugin-proposal-decorators", 51 | { 52 | "legacy": true 53 | } 54 | ], 55 | "@babel/plugin-proposal-function-sent", 56 | "@babel/plugin-proposal-export-namespace-from", 57 | "@babel/plugin-proposal-numeric-separator", 58 | "@babel/plugin-proposal-throw-expressions", 59 | "@babel/plugin-proposal-export-default-from", 60 | "@babel/plugin-proposal-logical-assignment-operators", 61 | "@babel/plugin-proposal-optional-chaining", 62 | [ 63 | "@babel/plugin-proposal-pipeline-operator", 64 | { 65 | "proposal": "minimal" 66 | } 67 | ], 68 | "@babel/plugin-proposal-nullish-coalescing-operator", 69 | "@babel/plugin-proposal-do-expressions" 70 | ] 71 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | insert_final_newline = true 4 | [*.{js,ts}] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_STORE 3 | lerna-debug.log 4 | yarn-error.log 5 | dist 6 | includes 7 | tsconfig.tsbuildinfo 8 | .env* 9 | coverage 10 | yarn.lock 11 | .rpt2_cache 12 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Development folders and files # 4 | ################################# 5 | .tmp/ 6 | node_modules/ 7 | package.json 8 | .travis.yml 9 | dist 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "arrowParens": "always", 6 | "overrides": [ 7 | { 8 | "files": "*.json", 9 | "options": { 10 | "parser": "json" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npmjs.org" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yuga Sun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tencent-sdk 2 | 3 | Tencent Cloud SDK 4 | 5 | ## Packages 6 | 7 | | Package Name | Introduction | Npm Link | 8 | | ----------------------- | ------------------------------------------------------ | ----------------------------------------------------------- | 9 | | `@tencent-sdk/capi` | [@tencent-sdk/capi](./packages/capi/README.md) | [Link](https://www.npmjs.com/package/@tencent-sdk/capi) | 10 | | `@tencent-sdk/login` | [@tencent-sdk/login](./packages/login/README.md) | [Link](https://www.npmjs.com/package/@tencent-sdk/login) | 11 | | `@tencent-sdk/faas` | [@tencent-sdk/faas](./packages/faas/README.md) | [Link](https://www.npmjs.com/package/@tencent-sdk/faas) | 12 | | `@tencent-sdk/cls` | [@tencent-sdk/cls](./packages/cls/README.md) | [Link](https://www.npmjs.com/package/@tencent-sdk/cls) | 13 | | `@tencent-sdk/capi-web` | [@tencent-sdk/capi-web](./packages/capi-web/README.md) | [Link](https://www.npmjs.com/package/@tencent-sdk/capi-web) | 14 | 15 | ## License 16 | 17 | [MIT](./LICENSE) 18 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | TENCENT_APP_ID: string; 4 | TENCENT_SECRET_ID: string; 5 | TENCENT_SECRET_KEY: string; 6 | TENCENT_SECRET_TOKEN?: string; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | require('dotenv').config({ path: join(__dirname, '.env.test') }); 3 | 4 | const isCI = !!process.env.CI; 5 | const mod = process.env.MODULE; 6 | 7 | const config = { 8 | verbose: true, 9 | silent: isCI, 10 | transform: { 11 | '^.+\\.tsx?$': 'ts-jest', 12 | }, 13 | globals: { 14 | 'ts-jest': { 15 | tsconfig: 'tsconfig.base.json', 16 | }, 17 | }, 18 | testTimeout: 60000, 19 | testEnvironment: 'node', 20 | testRegex: '/packages/.*/__tests__/.*\\.(test|spec)\\.(js|ts)$', 21 | testPathIgnorePatterns: ['/node_modules/', '/dist/', '/__tests__/fixtures/'], 22 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 23 | }; 24 | 25 | if (mod) { 26 | config.testRegex = `/${mod}/__tests__/.*\\.(test|spec)\\.(js|ts)$`; 27 | } 28 | 29 | module.exports = config; 30 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "independent", 4 | "useWorkspaces": true, 5 | "command": { 6 | "publish": { 7 | "allowBranch": ["master", "dev"], 8 | "message": "chore(release): lerna publish" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tencent-sdk", 3 | "private": true, 4 | "scripts": { 5 | "bootstrap": "lerna bootstrap", 6 | "build": "ts-node -P scripts/tsconfig.json scripts/bundle.ts umd,esm", 7 | "test": "jest", 8 | "clean": "lerna clean --yes && lerna run clean", 9 | "release": "yarn run build && lerna publish --exact", 10 | "prettier": "prettier --check '**/*.{ts,tsx,md}' --config .prettierrc", 11 | "prettier:fix": "prettier --write '**/*.{ts,tsx,md}' --config .prettierrc" 12 | }, 13 | "workspaces": [ 14 | "packages/*" 15 | ], 16 | "husky": { 17 | "hooks": { 18 | "pre-commit": "lint-staged" 19 | } 20 | }, 21 | "lint-staged": { 22 | "*.{ts,tsx,md}": [ 23 | "prettier -c" 24 | ] 25 | }, 26 | "devDependencies": { 27 | "@babel/cli": "^7.7.0", 28 | "@babel/core": "^7.7.2", 29 | "@babel/plugin-proposal-class-properties": "^7.7.0", 30 | "@babel/plugin-proposal-decorators": "^7.7.0", 31 | "@babel/plugin-proposal-do-expressions": "^7.6.0", 32 | "@babel/plugin-proposal-export-default-from": "^7.5.2", 33 | "@babel/plugin-proposal-export-namespace-from": "^7.5.2", 34 | "@babel/plugin-proposal-function-sent": "^7.7.0", 35 | "@babel/plugin-proposal-json-strings": "^7.2.0", 36 | "@babel/plugin-proposal-logical-assignment-operators": "^7.2.0", 37 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.4", 38 | "@babel/plugin-proposal-numeric-separator": "^7.2.0", 39 | "@babel/plugin-proposal-optional-chaining": "^7.6.0", 40 | "@babel/plugin-proposal-pipeline-operator": "^7.5.0", 41 | "@babel/plugin-proposal-throw-expressions": "^7.2.0", 42 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 43 | "@babel/plugin-syntax-import-meta": "^7.2.0", 44 | "@babel/plugin-transform-runtime": "^7.6.2", 45 | "@babel/polyfill": "^7.7.0", 46 | "@babel/preset-env": "^7.7.1", 47 | "@babel/preset-typescript": "^7.7.2", 48 | "@babel/runtime": "^7.7.2", 49 | "@types/node": "^12.12.6", 50 | "chalk": "^3.0.0", 51 | "cross-env": "^6.0.3", 52 | "dotenv": "^8.2.0", 53 | "fancy-log": "^1.3.3", 54 | "husky": "^3.0.9", 55 | "jest": "^26.6.3", 56 | "lerna": "^3.18.3", 57 | "lint-staged": "^9.5.0", 58 | "prettier": "^1.18.2", 59 | "rimraf": "^3.0.0", 60 | "rollup": "3.29.5", 61 | "rollup-plugin-alias": "^1.4.0", 62 | "rollup-plugin-commonjs": "8.4.1", 63 | "rollup-plugin-json": "^3.1.0", 64 | "rollup-plugin-node-builtins": "^2.1.2", 65 | "rollup-plugin-node-globals": "^1.4.0", 66 | "rollup-plugin-node-resolve": "^3.4.0", 67 | "rollup-plugin-typescript2": "0.17.1", 68 | "ts-jest": "^26.4.4", 69 | "ts-node": "^8.4.1", 70 | "tslint": "^5.20.1", 71 | "tslint-config-prettier": "^1.18.0", 72 | "typescript": "^4.1.2", 73 | "typescript-json-schema": "^0.44.1", 74 | "webpack": "^4.41.2", 75 | "webpack-command": "^0.5.0" 76 | }, 77 | "license": "MIT" 78 | } 79 | -------------------------------------------------------------------------------- /packages/capi-web/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.map 3 | tsconfig.tsbuildinfo 4 | test 5 | src 6 | tsconfig.json 7 | -------------------------------------------------------------------------------- /packages/capi-web/README.md: -------------------------------------------------------------------------------- 1 | ## Tencent Cound API For Browser 2 | 3 | Request tool for `iaas.cloud.tencent.com`, can only ussed in domain or subdomain of `cloud.tencent.com` 4 | 5 | ## Prepare 6 | 7 | Request tool for `iaas.cloud.tencent.com`, can only ussed in domain or subdomain of `cloud.tencent.com` 8 | 9 | Specify `document.domain` to `cloud.tencent.com`: 10 | 11 | ```html 12 | 15 | ``` 16 | 17 | Import `jQuery`: 18 | 19 | ```html 20 | 21 | 22 | ``` 23 | 24 | ## Usage 25 | 26 | Using by npm module: 27 | 28 | ``` 29 | $ npm i @tencent-sdk/capi-wen --save 30 | ``` 31 | 32 | ```js 33 | import { CapiRequest } from '@tencent-sdk/capi-web'; 34 | const data = await CapiRequest({ 35 | region: 'ap-guangzhou', 36 | serviceType: 'scf', 37 | action: 'ListFunctions', 38 | data: { 39 | Version: '2018-04-16', 40 | Namespace: 'default', 41 | Offset: 0, 42 | Limit: 20, 43 | }, 44 | }); 45 | ``` 46 | 47 | Using by script: 48 | 49 | ```html 50 | 51 | ``` 52 | 53 | ## License 54 | -------------------------------------------------------------------------------- /packages/capi-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tencent-sdk/capi-web", 3 | "version": "0.1.4", 4 | "description": "Tencent cloud api sdk for browser", 5 | "main": "dist/index.js", 6 | "node": "dist/index.js", 7 | "browser": "dist/index.js", 8 | "module": "dist/index.esm.js", 9 | "jsnext:main": "dist/index.esm.js", 10 | "types": "dist/index.d.ts", 11 | "typings": "dist/index.d.ts", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "scripts": { 16 | "test": "ts-node test/index.spec.ts", 17 | "clean": "rimraf ./dist tsconfig.tsbuildinfo" 18 | }, 19 | "keywords": [ 20 | "tencent-clound", 21 | "api", 22 | "web", 23 | "browser" 24 | ], 25 | "author": "yugasun", 26 | "license": "MIT", 27 | "directories": { 28 | "test": "test" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/yugasun/tencent-sdk.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/yugasun/tencent-sdk/issues" 36 | }, 37 | "homepage": "https://github.com/yugasun/tencent-sdk#readme" 38 | } 39 | -------------------------------------------------------------------------------- /packages/capi-web/src/index.ts: -------------------------------------------------------------------------------- 1 | interface CapiError { 2 | code?: string | number; 3 | Code?: string | number; 4 | cgwCode?: string | number; 5 | CgwCode?: string | number; 6 | Message?: string; 7 | } 8 | 9 | interface RequestData { 10 | Region?: string; 11 | Action?: string; 12 | Version?: string; 13 | } 14 | interface RequestParam { 15 | serviceType: string; 16 | action?: string; 17 | regionId?: number; 18 | data?: RequestData; 19 | } 20 | 21 | interface RequestOpts { 22 | isFormData?: string; 23 | clientTimeout?: number; 24 | } 25 | 26 | /** 27 | * whether is cam auth 28 | * @param {CapiError} e capi error 29 | */ 30 | export function isCamRefused(e: CapiError): boolean { 31 | e = e || {}; 32 | const code = (e.code || e.Code) + ''; 33 | return ( 34 | code === '4102' || 35 | code === '42' || 36 | code.indexOf('UnauthorizedOperation') !== -1 || 37 | code.indexOf('CamNoAuth') !== -1 38 | ); 39 | } 40 | 41 | /** 42 | * whether need login 43 | * @param {CapiError} e capi error 44 | */ 45 | export function needLogin(e: CapiError): boolean { 46 | e = e || {}; 47 | const code = (e.code || e.Code) + ''; 48 | return code === 'VERIFY_LOGIN_FAILED'; 49 | } 50 | 51 | /** 52 | * whther need auth 53 | * @param {CapiError} e capi error 54 | */ 55 | export function noAuth(e: CapiError): boolean { 56 | e = e || {}; 57 | const code = (e.code || e.Code || e.cgwCode || e.CgwCode) + ''; 58 | return code === '800200'; 59 | } 60 | 61 | const API_DOMAIN = `iaas.${location.hostname.substring( 62 | location.hostname.indexOf('.') + 1, 63 | )}`; 64 | const API_VERSION = '2017-03-12'; 65 | 66 | const proxyReady = (id = 'qcbase-proxy'): Promise => { 67 | let iframe: any; 68 | 69 | return new Promise((resolve, reject) => { 70 | iframe = document.getElementById(id); 71 | if (iframe) { 72 | // iframe 中的 domain 可能还没生效 73 | try { 74 | if (iframe.contentWindow.postSend) { 75 | return resolve(iframe.contentWindow.postSend); 76 | } 77 | } catch (e) {} 78 | } else { 79 | iframe = document.createElement('iframe'); 80 | iframe.id = id; 81 | iframe.style.display = 'none'; 82 | iframe.src = `//${API_DOMAIN}/proxy.html`; 83 | document.body.appendChild(iframe); 84 | } 85 | iframe.addEventListener('load', () => { 86 | try { 87 | resolve(iframe.contentWindow.postSend); 88 | } catch (e) { 89 | reject(e); 90 | } 91 | }); 92 | iframe.addEventListener('error', reject); 93 | }); 94 | }; 95 | 96 | async function request( 97 | url: string, 98 | params: RequestParam, 99 | opts?: RequestOpts, 100 | ): Promise { 101 | opts = opts || {}; 102 | const postSend = await proxyReady(); 103 | const data = await postSend(url, params, opts); 104 | return data; 105 | } 106 | 107 | /** 108 | * capi proxy request 109 | * @param {Object} data request data 110 | * @param {Object} opts request options 111 | * @return {Promise} 112 | */ 113 | export async function CapiRequest( 114 | params: RequestParam, 115 | opts?: RequestOpts, 116 | ): Promise { 117 | opts = opts || {}; 118 | 119 | const action = (params.data && params.data.Action) || params.action || ''; 120 | 121 | params.data = params.data || {}; 122 | if (!params.data.Version) { 123 | params.data.Version = API_VERSION; 124 | } 125 | 126 | const data = await request( 127 | `/cgi/capi?i=${params.serviceType}/${action}`, 128 | params, 129 | opts, 130 | ); 131 | if (data.code === 'VERIFY_LOGIN_FAILED') { 132 | return data; 133 | } 134 | const Response = 135 | data && data.data && (data.data.Response || data.data.data || null); 136 | if (Response && Response.Error) { 137 | throw Response.Error; 138 | } 139 | return Response; 140 | } 141 | -------------------------------------------------------------------------------- /packages/capi-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "../../typings/**/*.d.ts", "typings/**/*.d.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/capi/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.map 3 | tsconfig.tsbuildinfo 4 | test 5 | tests 6 | __test__ 7 | __tests__ 8 | src 9 | tsconfig.json 10 | -------------------------------------------------------------------------------- /packages/capi/README.md: -------------------------------------------------------------------------------- 1 | ## Tencent Cound API 2 | 3 | This is a basement api tool for all tencent cloud apis. 4 | 5 | ## Usage 6 | 7 | `Capi` is the only class for create a client request instance, and the instance only has one method `request`. 8 | You can use it like below: 9 | 10 | ```js 11 | import { Capi } from '@tencent-sdk/capi'; 12 | 13 | const client = new Capi({ 14 | Region: 'ap-guangzhou', 15 | SecretId: 'Please input your SecretId', 16 | SecretKey: 'Please input your SecretKey', 17 | Token: 'Please input your Token', 18 | ServiceType: 'tmt', 19 | }); 20 | try { 21 | const res = await client.request( 22 | { 23 | Action: 'TextTranslate', 24 | Version: '2018-03-21', 25 | SourceText: 'hello', 26 | Source: 'auto', 27 | Target: 'zh', 28 | ProjectId: 0, 29 | }, 30 | { 31 | debug: true, 32 | host: 'tmt.tencentcloudapi.com', 33 | }, 34 | ); 35 | console.log('res', res); 36 | } catch (e) { 37 | console.log(e); 38 | } 39 | ``` 40 | 41 | > This is a demo for using Tencent Machine Translator. 42 | 43 | ## Options 44 | 45 | ```js 46 | const client = new Capi(CapiOptions); 47 | client.request(RequestData, RequestOptions, isV3); 48 | ``` 49 | 50 | ### `CapiOptions` for Capi Constructor 51 | 52 | | Name | Description | Type | Required | Default | 53 | | --------------- | ----------------------------- | ------- | -------- | ---------------- | 54 | | ServiceType | tencent service type | string | true | '' | 55 | | Region | request region | string | true | ap-guangzhou | 56 | | SecretId | tencent account secret id | string | true | '' | 57 | | SecretKey | tencent account secret key | string | true | '' | 58 | | Token | tencent account token | string | false | '' | 59 | | debug | whether enable log debug info | boolean | false | false | 60 | | host | request host | string | false | false | 61 | | baseHost | request domain | string | false | 'api.qcloud.com' | 62 | | path | request path | string | false | '/' | 63 | | method | request method | string | false | 'POST' | 64 | | protocol | request protocol | string | false | 'https' | 65 | | SignatureMethod | request signature | string | false | 'sha1' | 66 | 67 | ### `RequestData` for reqeust method 68 | 69 | | Name | Description | Type | Required | Default | 70 | | ------------- | -------------------- | ------ | -------- | ------------ | 71 | | Action | api action | string | true | '' | 72 | | Version | api version | string | true | '2018-03-21' | 73 | | RequestClient | specify your service | string | false | 'TSS-CAPI' | 74 | | [propName] | left api parameters | any | false | '' | 75 | 76 | ### `RequestOptions` for reqeust method 77 | 78 | It is a copy from `CapiOptions`, if you set this, you can rewrite the properties in `CapiOptions`. 79 | 80 | ### `isV3` for request method 81 | 82 | `isV3` is used to specify to use version for authentication. 83 | 84 | > `true`: using `TC3-HMAC-SHA256` 85 | > `false`: using `HmacSHA256` or `Sha1` 86 | 87 | ## License 88 | 89 | MIT 90 | -------------------------------------------------------------------------------- /packages/capi/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Capi } from '../src'; 2 | 3 | describe('Capi', () => { 4 | const client = new Capi({ 5 | region: 'ap-guangzhou', 6 | secretId: process.env.TENCENT_SECRET_ID, 7 | secretKey: process.env.TENCENT_SECRET_KEY, 8 | token: process.env.TENCENT_TOKEN, 9 | serviceType: 'scf', 10 | version: '2018-04-16', 11 | }); 12 | 13 | test('[v1] should get api result success', async () => { 14 | const res = await client.request( 15 | { 16 | action: 'ListFunctions', 17 | }, 18 | { 19 | isV3: false, 20 | debug: true, 21 | }, 22 | ); 23 | 24 | expect(res).toEqual({ 25 | Response: { 26 | Functions: expect.any(Array), 27 | TotalCount: expect.any(Number), 28 | RequestId: expect.any(String), 29 | }, 30 | }); 31 | }); 32 | 33 | test('[v3] should get api result success', async () => { 34 | const res = await client.request( 35 | { 36 | action: 'ListFunctions', 37 | }, 38 | { 39 | isV3: true, 40 | debug: true, 41 | }, 42 | ); 43 | 44 | expect(res).toEqual({ 45 | Response: { 46 | Functions: expect.any(Array), 47 | TotalCount: expect.any(Number), 48 | RequestId: expect.any(String), 49 | }, 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/capi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tencent-sdk/capi", 3 | "version": "2.0.3", 4 | "description": "Tencent cloud api sdk", 5 | "main": "dist/index.js", 6 | "node": "dist/index.js", 7 | "browser": "dist/index.js", 8 | "module": "dist/index.esm.js", 9 | "jsnext:main": "dist/index.esm.js", 10 | "types": "dist/index.d.ts", 11 | "typings": "dist/index.d.ts", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "scripts": { 16 | "clean": "rimraf ./dist tsconfig.tsbuildinfo" 17 | }, 18 | "keywords": [ 19 | "tencent-clound", 20 | "api" 21 | ], 22 | "author": "yugasun", 23 | "license": "MIT", 24 | "dependencies": { 25 | "@tencent-sdk/common": "1.0.0", 26 | "got": "^11.8.2" 27 | }, 28 | "directories": { 29 | "test": "test" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/yugasun/tencent-sdk.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/yugasun/tencent-sdk/issues" 37 | }, 38 | "homepage": "https://github.com/yugasun/tencent-sdk#readme" 39 | } 40 | -------------------------------------------------------------------------------- /packages/capi/src/factory.ts: -------------------------------------------------------------------------------- 1 | import { deepClone, cleanEmptyValue } from '@tencent-sdk/common'; 2 | import { Capi } from './'; 3 | 4 | export enum ServiceType { 5 | /** API 网关服务 (apigateway) */ 6 | apigateway = 'apigateway', 7 | 8 | apigw = 'apigw', 9 | /** 云函数服务 (SCF) */ 10 | faas = 'scf', 11 | /** 视频处理服务 (MPS) */ 12 | mps = 'mps', 13 | /** 资源标签服务 (TAG) */ 14 | tag = 'tag', 15 | /** 内容分发 (CDN) */ 16 | cdn = 'cdn', 17 | /** 文件存储 (CFS) */ 18 | cfs = 'cfs', 19 | /** 域名解析服务 (CNS) */ 20 | cns = 'cns', 21 | /** */ 22 | domain = 'domain', 23 | /** MySQL 数据库 (CynosDB) */ 24 | cynosdb = 'cynosdb', 25 | /** Postgres 数据库 (Postgres) */ 26 | postgres = 'postgres', 27 | /** 私有网络 (VPC) */ 28 | vpc = 'vpc', 29 | /* 访问管理 (CAM) */ 30 | cam = 'cam', 31 | 32 | // 负载均衡 (CLB)*/ 33 | clb = 'clb', 34 | 35 | // 监控 */ 36 | monitor = 'monitor', 37 | } 38 | 39 | export interface ApiErrorOptions { 40 | message: string; 41 | stack?: string; 42 | type: string; 43 | reqId?: string | number; 44 | code?: string; 45 | displayMsg?: string; 46 | } 47 | 48 | export class CommonError extends Error { 49 | type: string; 50 | reqId?: string | number; 51 | code?: string; 52 | displayMsg: string; 53 | 54 | constructor({ 55 | type, 56 | message, 57 | stack, 58 | reqId, 59 | displayMsg, 60 | code, 61 | }: ApiErrorOptions) { 62 | super(message); 63 | this.type = type; 64 | if (stack) { 65 | this.stack = stack; 66 | } 67 | if (reqId) { 68 | this.reqId = reqId; 69 | } 70 | if (code) { 71 | this.code = code; 72 | } 73 | this.displayMsg = displayMsg ?? message; 74 | return this; 75 | } 76 | } 77 | 78 | interface ApiFactoryOptions { 79 | serviceType: ServiceType; 80 | version: string; 81 | actions: ACTIONS_T; 82 | 83 | debug?: boolean; 84 | isV3?: boolean; 85 | host?: string; 86 | path?: string; 87 | requestClient?: string; 88 | 89 | customHandler?: (action: string, res: any) => any; 90 | responseHandler?: (res: any) => any; 91 | errorHandler?: (action: string, res: any) => any; 92 | } 93 | 94 | export function ApiFactory({ 95 | debug = false, 96 | isV3 = false, 97 | actions, 98 | serviceType, 99 | host, 100 | path, 101 | version, 102 | customHandler, 103 | responseHandler = (res: any) => res, 104 | errorHandler, 105 | 106 | requestClient = 'TENCENT_SDK', 107 | }: ApiFactoryOptions) { 108 | const APIS: Record< 109 | ACTIONS_T[number], 110 | (capi: Capi, inputs: any) => any 111 | > = {} as any; 112 | actions.forEach((action: ACTIONS_T[number]) => { 113 | APIS[action] = async (capi: Capi, inputs: any) => { 114 | inputs = deepClone(inputs); 115 | 116 | const reqData = cleanEmptyValue({ 117 | action, 118 | version, 119 | ...inputs, 120 | }); 121 | inputs = cleanEmptyValue(inputs); 122 | try { 123 | const res = await capi.request(reqData, { 124 | isV3, 125 | debug, 126 | requestClient, 127 | host: host || `${serviceType}.tencentcloudapi.com`, 128 | path: path || '/', 129 | }); 130 | // Customize response handler 131 | if (customHandler) { 132 | return customHandler(action, res); 133 | } 134 | const { Response } = res; 135 | if (Response?.Error?.Code) { 136 | if (errorHandler) { 137 | return errorHandler(action, Response); 138 | } 139 | throw new CommonError({ 140 | type: `API_${serviceType.toUpperCase()}_${action}`, 141 | message: `${Response.Error.Message} (reqId: ${Response.RequestId})`, 142 | reqId: Response.RequestId, 143 | code: Response.Error.Code, 144 | }); 145 | } 146 | return responseHandler(Response); 147 | } catch (e) { 148 | throw new CommonError({ 149 | type: `API_${serviceType.toUpperCase()}_${action}`, 150 | message: e.message, 151 | stack: e.stack, 152 | reqId: e.reqId, 153 | code: e.code, 154 | }); 155 | } 156 | }; 157 | }); 158 | 159 | return APIS; 160 | } 161 | -------------------------------------------------------------------------------- /packages/capi/src/index.ts: -------------------------------------------------------------------------------- 1 | import got, { Options, Response } from 'got'; 2 | import { logger, pascalCaseProps, querystring } from '@tencent-sdk/common'; 3 | import { tencentSign, tencentSignV1 } from './utils'; 4 | 5 | export { tencentSign, tencentSignV1 } from './utils'; 6 | 7 | export * from './factory'; 8 | 9 | export interface CapiOptions { 10 | isPascalCase?: boolean; // whether api parameter need use pascalCase to handler 11 | isV3?: boolean; // whether to use version3 sign method 12 | debug?: boolean; // whether enable log debug info 13 | host?: string; // request host 14 | baseHost?: string; // request domain, default: api.qcloud.com 15 | path?: string; // request path, default: / 16 | method?: string; // request method, default: POST 17 | protocol?: string; // request protocol, default: https 18 | timeout?: number; // request timeout in miliseconds 19 | 20 | serviceType: string; // tencent service type, eg: apigateway 21 | version?: string; // tencent service type, eg: apigateway 22 | region: string; // request region, default: ap-guangzhou 23 | secretId: string; // tencent account secret id 24 | secretKey: string; // tencent account secret key 25 | token?: string; // tencent account token 26 | signatureMethod?: string; // request signature method, default: sha1 27 | requestClient?: string; // request client 28 | } 29 | 30 | export interface RequestData { 31 | action: string; // request action 32 | requestClient?: string; // optional, just to specify your service 33 | version?: string; // api version, default: 2018-03-21 34 | [propName: string]: any; // left api parameters 35 | } 36 | 37 | export interface RequestOptions { 38 | isV3?: boolean; // whether to use version3 sign method 39 | debug?: boolean; // whether enable log debug info 40 | host?: string; // request host 41 | baseHost?: string; // request domain, default: api.qcloud.com 42 | path?: string; // request path, default: / 43 | method?: string; // request method, default: POST 44 | protocol?: string; // request protocol, default: https 45 | timeout?: number; // request timeout in miliseconds 46 | requestClient?: string; // request client 47 | } 48 | 49 | export interface CapiInstance { 50 | request: ( 51 | data: RequestData, 52 | opts?: RequestOptions, 53 | isV3?: boolean, 54 | ) => Promise; 55 | } 56 | 57 | export class Capi implements CapiInstance { 58 | options: CapiOptions; 59 | defaultOptions: CapiOptions = { 60 | path: '/', // api request path 61 | method: 'POST', 62 | protocol: 'https', 63 | baseHost: 'tencentcloudapi.com', 64 | serviceType: '', 65 | secretId: '', 66 | secretKey: '', 67 | region: 'ap-guangzhou', 68 | signatureMethod: 'sha1', // sign algorithm, default is sha1 69 | isPascalCase: true, 70 | }; 71 | 72 | constructor(options: CapiOptions) { 73 | this.options = Object.assign(this.defaultOptions, options); 74 | } 75 | 76 | async request( 77 | data: RequestData, 78 | opts: RequestOptions = this.defaultOptions, 79 | isV3 = false, 80 | ) { 81 | const options = Object.assign(this.options, opts); 82 | options.requestClient = 83 | options.requestClient || data.requestClient || 'TENCENT_SDK_CAPI'; 84 | let { action, Action, Version, version, ...restData } = data; 85 | action = action || Action; 86 | version = version || Version; 87 | 88 | let reqOption: Options = { 89 | url: '', 90 | method: 'GET', 91 | responseType: 'json', 92 | }; 93 | if (isV3 || opts.isV3) { 94 | const { url, payload, authorization, timestamp, host } = tencentSign( 95 | this.options.isPascalCase ? pascalCaseProps(restData) : restData, 96 | options, 97 | ); 98 | reqOption = { 99 | url, 100 | method: 'POST', 101 | responseType: 'json', 102 | headers: { 103 | 'Content-Type': 'application/json', 104 | Authorization: authorization, 105 | Host: host, 106 | 'X-TC-Action': action, 107 | 'X-TC-Version': version || options.version, 108 | 'X-TC-Timestamp': timestamp, 109 | 'X-TC-Region': options.region, 110 | }, 111 | json: payload, 112 | }; 113 | reqOption.headers!['X-TC-RequestClient'] = options.requestClient; 114 | if (this.options.token) { 115 | reqOption.headers!['X-TC-Token'] = this.options.token; 116 | } 117 | } else { 118 | const reqData = { 119 | ...restData, 120 | Action: action, 121 | }; 122 | const { url, method, payload } = tencentSignV1( 123 | this.options.isPascalCase ? pascalCaseProps(reqData) : reqData, 124 | options, 125 | ); 126 | reqOption = { 127 | url, 128 | method, 129 | responseType: 'json', 130 | headers: { 131 | 'Content-Type': 'application/json', 132 | }, 133 | }; 134 | 135 | if (method === 'POST') { 136 | reqOption.form = payload; 137 | } else { 138 | reqOption.url += '?' + querystring(payload); 139 | } 140 | } 141 | 142 | if (options.timeout) { 143 | reqOption.timeout = options.timeout; 144 | } 145 | // debug request option 146 | if (options.debug) { 147 | logger('Request Option', JSON.stringify(reqOption)); 148 | } 149 | 150 | const { url, ...restOptions } = reqOption; 151 | const { body } = (await got(url!, restOptions)) as Response; 152 | return body as any; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /packages/capi/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { createHash, createHmac } from 'crypto'; 2 | import { Method } from 'got'; 3 | import { getUnixTime, getDate, flatten } from '@tencent-sdk/common'; 4 | import { CapiOptions } from './index'; 5 | 6 | export interface Payload { 7 | Region?: string; 8 | SecretId?: string; 9 | Timestamp?: number | string; 10 | Nonce?: number; 11 | [propName: string]: any; 12 | } 13 | 14 | export interface HostParams { 15 | serviceType: string; 16 | region: string; 17 | host: string | undefined; 18 | baseHost: string | undefined; 19 | path?: string; 20 | protocol?: string; 21 | } 22 | 23 | export interface TencentSignResult { 24 | url: string; 25 | payload: Payload; 26 | host: string; 27 | authorization: string; 28 | timestamp: string | string[] | undefined; 29 | } 30 | 31 | export interface TencentSignResultV1 { 32 | url: string; 33 | method: Method; 34 | payload: Payload; 35 | } 36 | 37 | export function getHost( 38 | { host, serviceType, region, baseHost }: HostParams, 39 | isV1 = false, 40 | ) { 41 | if (!host) { 42 | host = `${serviceType}${isV1 ? '' : `.${region}`}.${baseHost}`; 43 | } 44 | return host; 45 | } 46 | 47 | export function getUrl(opts: HostParams, isV1 = false) { 48 | const host = getHost(opts, isV1); 49 | const path = opts.path || '/'; 50 | 51 | return `${opts.protocol || 'https'}://${host}${path}`; 52 | } 53 | 54 | export function sign( 55 | str: string, 56 | secretKey: Buffer, 57 | algorithm: string = 'sha256', 58 | ): Buffer { 59 | const hmac = createHmac(algorithm, secretKey); 60 | return hmac.update(Buffer.from(str, 'utf8')).digest(); 61 | } 62 | 63 | /** 64 | * generate tencent cloud sign result 65 | * 66 | * @param {Payload} payload 67 | * @param {CapiOptions} options 68 | * @returns {TencentSignResult} 69 | */ 70 | export function tencentSign( 71 | payload: Payload, 72 | options: CapiOptions, 73 | ): TencentSignResult { 74 | const hostParams: HostParams = { 75 | host: options.host, 76 | path: options.path, 77 | protocol: options.protocol, 78 | baseHost: options.baseHost, 79 | serviceType: options.serviceType, 80 | region: options.region, 81 | }; 82 | const url = getUrl(hostParams); 83 | const host = getHost(hostParams); 84 | const d = new Date(); 85 | const timestamp = String(getUnixTime(d)); 86 | const date = getDate(d); 87 | const algorithm = 'TC3-HMAC-SHA256'; 88 | 89 | // 1. create Canonical request string 90 | const httpRequestMethod = (options.method || 'POST').toUpperCase(); 91 | const canonicalURI = '/'; 92 | const canonicalQueryString = ''; 93 | const canonicalHeaders = `content-type:application/json\nhost:${host}\n`; 94 | const signedHeaders = 'content-type;host'; 95 | const hashedRequestPayload = createHash('sha256') 96 | .update(JSON.stringify(payload)) 97 | .digest('hex'); 98 | const canonicalRequest = `${httpRequestMethod}\n${canonicalURI}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${hashedRequestPayload}`; 99 | 100 | // 2. create string to sign 101 | const credentialScope = `${date}/${options.serviceType}/tc3_request`; 102 | const hashedCanonicalRequest = createHash('sha256') 103 | .update(canonicalRequest) 104 | .digest('hex'); 105 | const stringToSign = `${algorithm}\n${timestamp}\n${credentialScope}\n${hashedCanonicalRequest}`; 106 | 107 | // 3. calculate signature 108 | const secretDate = sign(date, Buffer.from(`TC3${options.secretKey}`, 'utf8')); 109 | const secretService = sign(options.serviceType, secretDate); 110 | const secretSigning = sign('tc3_request', secretService); 111 | const signature = createHmac('sha256', secretSigning) 112 | .update(Buffer.from(stringToSign, 'utf8')) 113 | .digest('hex'); 114 | 115 | // 4. create authorization 116 | const authorization = `${algorithm} Credential=${options.secretId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`; 117 | 118 | return { 119 | url, 120 | payload, 121 | host, 122 | authorization, 123 | timestamp, 124 | }; 125 | } 126 | 127 | /** 128 | * version1: generate tencent cloud sign result 129 | * 130 | * @param {Payload} payload 131 | * @param {CapiOptions} options 132 | * @returns {TencentSignResultV1} 133 | */ 134 | export function tencentSignV1( 135 | payload: Payload, 136 | options: CapiOptions, 137 | ): TencentSignResultV1 { 138 | const hostParams: HostParams = { 139 | host: options.host, 140 | path: options.path, 141 | protocol: options.protocol, 142 | baseHost: options.baseHost, 143 | serviceType: options.serviceType, 144 | region: options.region, 145 | }; 146 | const url = getUrl(hostParams, true); 147 | const Host = getHost(hostParams, true); 148 | const d = new Date(); 149 | const Timestamp = getUnixTime(d); 150 | const Nonce = Math.round(Math.random() * 65535); 151 | 152 | payload.Region = options.region; 153 | payload.Nonce = Nonce; 154 | payload.Timestamp = Timestamp; 155 | payload.SecretId = options.secretId; 156 | payload.Version = payload.Version || options.version; 157 | payload.RequestClient = options.requestClient; 158 | 159 | if (options.token) { 160 | payload.Token = options.token; 161 | } 162 | if (options.signatureMethod === 'sha256') { 163 | payload.SignatureMethod = 'HmacSHA256'; 164 | } 165 | 166 | payload = flatten(payload); 167 | 168 | const keys = Object.keys(payload).sort(); 169 | const method = (options.method || 'POST').toUpperCase() as Method; 170 | 171 | let qstr = ''; 172 | keys.forEach((key) => { 173 | if (key === '') { 174 | return; 175 | } 176 | key = key.indexOf('_') ? key.replace(/_/g, '.') : key; 177 | let val = payload[key]; 178 | if (method === 'POST' && val && val[0] === '@') { 179 | return; 180 | } 181 | if ( 182 | val === undefined || 183 | val === null || 184 | (typeof val === 'number' && isNaN(val)) 185 | ) { 186 | val = ''; 187 | } 188 | qstr += `&${key}=${val}`; 189 | }); 190 | 191 | qstr = qstr.slice(1); 192 | 193 | payload.Signature = sign( 194 | `${method}${Host}${options.path}?${qstr}`, 195 | Buffer.from(options.secretKey, 'utf8'), 196 | options.signatureMethod, 197 | ).toString('base64'); 198 | 199 | return { 200 | url, 201 | method, 202 | payload, 203 | }; 204 | } 205 | -------------------------------------------------------------------------------- /packages/capi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "../../typings/**/*.d.ts", "typings/**/*.d.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/cls/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.map 3 | tsconfig.tsbuildinfo 4 | test 5 | tests 6 | __test__ 7 | __tests__ 8 | src 9 | tsconfig.json 10 | -------------------------------------------------------------------------------- /packages/cls/README.md: -------------------------------------------------------------------------------- 1 | ## Tencent Cloud CLS SDK 2 | 3 | This is a SDK tool for [Tencent Cloud CLS](https://console.cloud.tencent.com/cls) service. 4 | 5 | ## Usage 6 | 7 | `Cls` is the only class for create a client request instance. 8 | You can use it like below: 9 | 10 | ```js 11 | import { Cls } from '@tencent-sdk/cls'; 12 | 13 | const client = new Cls({ 14 | region: 'ap-guangzhou', 15 | secretId: 'Please input your SecretId', 16 | secretKey: 'Please input your SecretKey', 17 | token: 'Please input your Token', 18 | debug: false, 19 | }); 20 | ``` 21 | 22 | Support methods: 23 | 24 | - [getLogsetList()](#getLogsetList) 25 | - [createLogset()](#createLogset) 26 | - [getLogset()](#getLogset) 27 | - [deleteLogset()](#deleteLogset) 28 | - [getTopicList()](#getTopicList) 29 | - [createTopic()](#createTopic) 30 | - [getTopic()](#getTopic) 31 | - [deleteTopic()](#deleteTopic) 32 | - [updateIndex()](#updateIndex) 33 | - [getIndex()](#getIndex) 34 | 35 | ### getLogsetList 36 | 37 | Get logset list: 38 | 39 | ```js 40 | const res = await client.getLogsetList(); 41 | ``` 42 | 43 | ### createLogset 44 | 45 | Create logset: 46 | 47 | ```js 48 | const res = await client.createLogset({ 49 | logset_name: 'cls-test', 50 | period: 7, 51 | }); 52 | ``` 53 | 54 | ### getLogset 55 | 56 | Get logset: 57 | 58 | ```js 59 | const res = await client.getLogset({ 60 | logset_id: 'xxx-xxx', 61 | }); 62 | ``` 63 | 64 | ### deleteLogset 65 | 66 | Delete logset: 67 | 68 | ```js 69 | const res = await client.deleteLogset({ 70 | logset_id: 'xxx-xxx', 71 | }); 72 | ``` 73 | 74 | ### getTopicList 75 | 76 | Get topic list: 77 | 78 | ```js 79 | const res = await client.getTopicList(); 80 | ``` 81 | 82 | ### createTopic 83 | 84 | Create topic: 85 | 86 | ```js 87 | const res = await client.createTopic({ 88 | logset_id: 'xxx-xxx', 89 | topic_name: 'cls-test-topic', 90 | }); 91 | ``` 92 | 93 | ### getTopic 94 | 95 | Get topic: 96 | 97 | ```js 98 | const res = await client.getTopic({ 99 | topic_id: 'xxx-xxx', 100 | }); 101 | ``` 102 | 103 | ### deleteTopic 104 | 105 | Delete topic: 106 | 107 | ```js 108 | const res = await client.deleteTopic({ 109 | topic_id: 'xxx-xxx', 110 | }); 111 | ``` 112 | 113 | ### updateIndex 114 | 115 | Update topic index: 116 | 117 | ```js 118 | const res = await client.updateIndex({ 119 | topic_id: 'xxx-xxx', 120 | effective: true, 121 | rule: { 122 | full_text: { 123 | case_sensitive: true, 124 | tokenizer: '!@#%^&*()_="\', <>/?|\\;:\n\t\r[]{}', 125 | }, 126 | key_value: { 127 | case_sensitive: true, 128 | keys: ['SCF_RetMsg'], 129 | types: ['text'], 130 | tokenizers: [' '], 131 | }, 132 | }, 133 | }); 134 | ``` 135 | 136 | ### getIndex 137 | 138 | Get topic index: 139 | 140 | ```js 141 | const res = await client.getIndex({ 142 | topic_id: 'xxx-xxx', 143 | }); 144 | ``` 145 | 146 | ### Custom methods 147 | 148 | If you need methods expect above list, you can use `client.request()` like below: 149 | 150 | ```js 151 | // create cls shipper 152 | const res = await client.request({ 153 | path: '/shipper', 154 | method: 'POST', 155 | data: { 156 | topic_id: 'xxxx-xx-xx-xx-xxxxxxxx', 157 | bucket: 'test-1250000001', 158 | prefix: 'test', 159 | shipper_name: 'myname', 160 | interval: 300, 161 | max_size: 100, 162 | partition: '%Y%m%d', 163 | compress: { 164 | format: 'none', 165 | }, 166 | content: { 167 | format: 'csv', 168 | csv_info: { 169 | print_key: true, 170 | keys: ['key1', 'key2'], 171 | delimiter: '|', 172 | escape_char: "'", 173 | non_existing_field: 'null', 174 | }, 175 | }, 176 | }, 177 | }); 178 | ``` 179 | 180 | ## Options 181 | 182 | ```js 183 | const client = new Cls(ClsOptions); 184 | ``` 185 | 186 | ### `ClsOptions` for Cls Construct 187 | 188 | | Name | Description | Type | Required | Default | 189 | | --------- | ------------------------------------ | ------- | -------- | ------------ | 190 | | region | request region | string | true | ap-guangzhou | 191 | | secretId | tencent account secret id | string | true | '' | 192 | | secretKey | tencent account secret key | string | true | '' | 193 | | token | tencent account token | string | false | '' | 194 | | debug | whether enable log debug info | boolean | false | false | 195 | | timeout | request timeout, unit `ms` | number | false | 5000 | 196 | | expire | expire time for signature, unit `ms` | number | false | 300000 | 197 | 198 | ## License 199 | 200 | MIT 201 | -------------------------------------------------------------------------------- /packages/cls/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Cls } from '../src'; 2 | 3 | describe('Cls', () => { 4 | const client = new Cls({ 5 | region: 'ap-guangzhou', 6 | secretId: process.env.TENCENT_SECRET_ID, 7 | secretKey: process.env.TENCENT_SECRET_KEY, 8 | token: process.env.TENCENT_TOKEN, 9 | debug: true, 10 | }); 11 | 12 | let logset_id: string = ''; 13 | let topic_id: string = ''; 14 | 15 | test('create logset', async () => { 16 | const res = await client.createLogset({ 17 | logset_name: 'cls-test', 18 | period: 7, 19 | }); 20 | expect(res).toEqual({ 21 | requestId: expect.any(String), 22 | logset_id: expect.any(String), 23 | }); 24 | 25 | logset_id = res.logset_id; 26 | }); 27 | 28 | test('get logset', async () => { 29 | const res = await client.getLogset({ 30 | logset_id, 31 | }); 32 | expect(res).toEqual({ 33 | requestId: expect.any(String), 34 | create_time: expect.any(String), 35 | logset_id: logset_id, 36 | logset_name: 'cls-test', 37 | period: 7, 38 | topics_number: 0, 39 | }); 40 | 41 | logset_id = res.logset_id; 42 | }); 43 | 44 | test('get logset list', async () => { 45 | const res = await client.getLogsetList(); 46 | expect(res).toEqual({ 47 | requestId: expect.any(String), 48 | logsets: expect.any(Array), 49 | }); 50 | 51 | const [exist] = res.logsets.filter( 52 | (item: { logset_id: string }) => item.logset_id === logset_id, 53 | ); 54 | expect(exist).toEqual({ 55 | create_time: expect.any(String), 56 | logset_id, 57 | logset_name: 'cls-test', 58 | period: 7, 59 | topics_number: 0, 60 | }); 61 | }); 62 | 63 | test('create topic', async () => { 64 | const res = await client.createTopic({ 65 | logset_id, 66 | topic_name: 'cls-test-topic', 67 | }); 68 | expect(res).toEqual({ 69 | requestId: expect.any(String), 70 | topic_id: expect.any(String), 71 | }); 72 | 73 | topic_id = res.topic_id; 74 | }); 75 | 76 | test('get topic', async () => { 77 | const res = await client.getTopic({ 78 | topic_id, 79 | }); 80 | expect(res.topic_id).toBe(topic_id); 81 | expect(res.topic_name).toBe('cls-test-topic'); 82 | }); 83 | 84 | test('update index', async () => { 85 | const res = await client.updateIndex({ 86 | topic_id, 87 | effective: true, 88 | rule: { 89 | full_text: { 90 | case_sensitive: true, 91 | tokenizer: '!@#%^&*()_="\', <>/?|\\;:\n\t\r[]{}', 92 | }, 93 | key_value: { 94 | case_sensitive: true, 95 | keys: ['SCF_RetMsg'], 96 | types: ['text'], 97 | tokenizers: [' '], 98 | }, 99 | }, 100 | }); 101 | 102 | expect(res).toEqual({ 103 | requestId: expect.any(String), 104 | success: true, 105 | }); 106 | }); 107 | 108 | test('get index', async () => { 109 | const res = await client.getIndex({ 110 | topic_id, 111 | }); 112 | 113 | expect(res).toEqual({ 114 | requestId: expect.any(String), 115 | effective: true, 116 | rule: { 117 | full_text: { 118 | case_sensitive: true, 119 | tokenizer: `!@#%^&*()_="', <>/?|\\;:\n\t\r[]{}`, 120 | }, 121 | key_value: { 122 | case_sensitive: true, 123 | template_type: 'static', 124 | keys: ['SCF_RetMsg'], 125 | types: ['text'], 126 | tokenizers: [' '], 127 | }, 128 | }, 129 | topic_id, 130 | }); 131 | }); 132 | 133 | test('delete topic', async () => { 134 | const res = await client.deleteTopic({ 135 | topic_id, 136 | }); 137 | console.log('res', res); 138 | 139 | // TODO: cloud api bug 140 | // expect(res).toEqual({ 141 | // requestId: expect.any(String), 142 | // success: true, 143 | // }); 144 | expect(true).toBe(true); 145 | }); 146 | 147 | test('delete logset', async () => { 148 | const res = await client.deleteLogset({ 149 | logset_id, 150 | }); 151 | expect(res).toEqual({ 152 | requestId: expect.any(String), 153 | success: true, 154 | }); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /packages/cls/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tencent-sdk/cls", 3 | "version": "1.0.1", 4 | "description": "Tencent cloud cls sdk", 5 | "main": "dist/index.js", 6 | "node": "dist/index.js", 7 | "browser": "dist/index.js", 8 | "module": "dist/index.esm.js", 9 | "jsnext:main": "dist/index.esm.js", 10 | "types": "dist/index.d.ts", 11 | "typings": "dist/index.d.ts", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "scripts": { 16 | "test": "ts-node test/index.test.ts", 17 | "clean": "rimraf ./dist tsconfig.tsbuildinfo" 18 | }, 19 | "keywords": [ 20 | "tencent-cloud", 21 | "cls" 22 | ], 23 | "author": "yugasun", 24 | "license": "MIT", 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/yugasun/tencent-sdk.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/yugasun/tencent-sdk/issues" 31 | }, 32 | "homepage": "https://github.com/yugasun/tencent-sdk#readme", 33 | "dependencies": { 34 | "@tencent-sdk/common": "1.0.0", 35 | "got": "^11.8.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/cls/src/index.ts: -------------------------------------------------------------------------------- 1 | import got, { Options } from 'got'; 2 | import { logger, querystring } from '@tencent-sdk/common'; 3 | import { tencentSign } from './utils'; 4 | import { 5 | ClsOptions, 6 | ApiResponse, 7 | CreateTopicData, 8 | UpdateTopicData, 9 | UpdateIndexData, 10 | SearchLogData, 11 | RequestOptions, 12 | } from './typings'; 13 | 14 | export { tencentSign } from './utils'; 15 | 16 | export class Cls { 17 | options: ClsOptions; 18 | constructor(options: ClsOptions) { 19 | this.options = options; 20 | 21 | this.options.region = this.options.region || 'ap-guangzhou'; 22 | this.options.expire = this.options.expire || 300000; 23 | } 24 | 25 | /** 26 | * get logsets list 27 | */ 28 | async getLogsetList(): Promise { 29 | const res = await this.request({ 30 | method: 'GET', 31 | path: '/logsets', 32 | }); 33 | return res; 34 | } 35 | 36 | /** 37 | * get logset detail 38 | * @param logset_id string 39 | */ 40 | async getLogset(data: { logset_id: string }): Promise { 41 | const res = await this.request({ 42 | method: 'GET', 43 | path: '/logset', 44 | query: data, 45 | }); 46 | return res; 47 | } 48 | 49 | /** 50 | * create logset 51 | * @param data 52 | */ 53 | async createLogset(data: { 54 | logset_name: string; 55 | period: number; 56 | }): Promise { 57 | const res = await this.request({ 58 | method: 'POST', 59 | path: '/logset', 60 | data, 61 | }); 62 | return res; 63 | } 64 | 65 | /** 66 | * update logset 67 | * @param data 68 | */ 69 | async updateLogset(data: { 70 | logset_id: string; 71 | logset_name: string; 72 | period: number; 73 | }): Promise { 74 | const res = await this.request({ 75 | method: 'PUT', 76 | path: '/logset', 77 | data, 78 | }); 79 | return res; 80 | } 81 | 82 | /** 83 | * dalete logset 84 | * @param data 85 | */ 86 | async deleteLogset(data: { logset_id: string }): Promise { 87 | const res = await this.request({ 88 | method: 'DELETE', 89 | path: '/logset', 90 | query: data, 91 | }); 92 | return res; 93 | } 94 | 95 | /** 96 | * create topic 97 | * @param data 98 | */ 99 | async createTopic(data: CreateTopicData): Promise { 100 | const res = await this.request({ 101 | method: 'POST', 102 | path: '/topic', 103 | data, 104 | }); 105 | return res; 106 | } 107 | 108 | /** 109 | * get topic 110 | * @param data 111 | */ 112 | async getTopic(data: { topic_id: string }): Promise { 113 | const res = await this.request({ 114 | method: 'GET', 115 | path: '/topic', 116 | query: data, 117 | }); 118 | return res; 119 | } 120 | 121 | /** 122 | * get topic 123 | * @param data 124 | */ 125 | async getTopicList(data: { logset_id: string }): Promise { 126 | const res = await this.request({ 127 | method: 'GET', 128 | path: '/topics', 129 | query: data, 130 | }); 131 | return res; 132 | } 133 | 134 | /** 135 | * update topic 136 | * @param data 137 | */ 138 | async updateTopic(data: UpdateTopicData): Promise { 139 | const res = await this.request({ 140 | method: 'PUT', 141 | path: '/topic', 142 | data, 143 | }); 144 | return res; 145 | } 146 | 147 | /** 148 | * delete topic 149 | * @param data 150 | */ 151 | async deleteTopic(data: { topic_id: string }): Promise { 152 | const res = await this.request({ 153 | method: 'DELETE', 154 | path: '/topic', 155 | query: data, 156 | }); 157 | return res; 158 | } 159 | 160 | async getIndex(data: { topic_id: string }): Promise { 161 | const res = await this.request({ 162 | method: 'GET', 163 | path: '/index', 164 | query: data, 165 | }); 166 | return res; 167 | } 168 | 169 | /** 170 | * update index 171 | * @param data 172 | */ 173 | async updateIndex(data: UpdateIndexData): Promise { 174 | const res = await this.request({ 175 | method: 'PUT', 176 | path: '/index', 177 | data, 178 | }); 179 | return res; 180 | } 181 | 182 | async searchLog(data: SearchLogData): Promise { 183 | const res = await this.request({ 184 | method: 'GET', 185 | path: '/searchlog', 186 | query: data, 187 | }); 188 | 189 | return res; 190 | } 191 | 192 | async request({ 193 | method, 194 | path, 195 | query, 196 | data, 197 | }: RequestOptions): Promise { 198 | const { options } = this; 199 | const baseHost = options.baseHost || 'cls.tencentcs.com'; 200 | const host = `${options.region}.${baseHost}`; 201 | const authorization = tencentSign({ 202 | secretId: options.secretId, 203 | secretKey: options.secretKey, 204 | // default 5 minutes 205 | expire: options.expire || 300000, 206 | method, 207 | path, 208 | parameters: query || {}, 209 | headers: { 210 | Host: host, 211 | }, 212 | }); 213 | 214 | let url = `https://${host}${path}`; 215 | 216 | const reqOption: Options = { 217 | url, 218 | method, 219 | responseType: 'json', 220 | headers: { 221 | 'Content-Type': 'application/json', 222 | Authorization: authorization, 223 | Host: host, 224 | }, 225 | json: data || undefined, 226 | }; 227 | if (query) { 228 | reqOption.url = `https://${host}${path}?${querystring(query, true)}`; 229 | } 230 | 231 | if (options.token) { 232 | if (!reqOption.headers) { 233 | reqOption.headers = {}; 234 | } 235 | reqOption.headers['x-cls-token'] = options.token; 236 | } 237 | if (options.timeout) { 238 | reqOption.timeout = options.timeout; 239 | } 240 | // debug request option 241 | if (options.debug) { 242 | logger('Request Option', JSON.stringify(reqOption)); 243 | } 244 | 245 | try { 246 | const { headers, body, statusCode } = (await got( 247 | reqOption, 248 | )) as ApiResponse; 249 | const reqId = headers && headers['x-cls-requestid']; 250 | if (!body) { 251 | return { 252 | requestId: reqId, 253 | success: statusCode === 200, 254 | }; 255 | } 256 | body.requestId = reqId; 257 | return body as ApiResponse; 258 | } catch (e) { 259 | const response = e.response || {}; 260 | const reqId = response.headers && response.headers['x-cls-requestid']; 261 | 262 | return { 263 | requestId: reqId, 264 | error: { 265 | message: reqId ? `${e.message} (reqId: ${reqId})` : e.message, 266 | }, 267 | }; 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /packages/cls/src/typings/index.ts: -------------------------------------------------------------------------------- 1 | import { Method } from 'got'; 2 | import { KeyValueObject } from '@tencent-sdk/common'; 3 | export interface ClsOptions { 4 | baseHost?: string; // base host 5 | region: string; // request region, default: ap-guangzhou 6 | secretId: string; // tencent account secret id 7 | secretKey: string; // tencent account secret key 8 | token?: string; // tencent account token 9 | debug?: boolean; // whether enable log debug info 10 | timeout?: number; // request timeout in miliseconds 11 | expire?: number; // sign expire time in miliseconds 12 | } 13 | 14 | export interface RequestOptions { 15 | debug?: boolean; // whether enable log debug info 16 | path: string; // request path 17 | method: Method; // request method 18 | query?: KeyValueObject; 19 | data?: KeyValueObject; 20 | headers?: KeyValueObject; 21 | options?: ClsOptions; 22 | } 23 | 24 | export interface ApiError { 25 | message: string; 26 | } 27 | 28 | export interface ApiResponse { 29 | error?: ApiError; 30 | [prop: string]: any; 31 | } 32 | 33 | export interface TopicExtractRule { 34 | // 时间字段的 key 名字,time_key 和 time_format 必须成对出现 35 | time_key?: string; 36 | // 时间字段的格式,参考 C 语言的strftime函数对于时间的格式说明 37 | time_format?: string; 38 | // 分隔符类型日志的分隔符,只有log_type为delimiter_log时有效 39 | delimiter?: string; 40 | // 整条日志匹配规则,只有log_type为fullregex_log时有效 41 | log_regex?: string; 42 | // 行首匹配规则,只有log_type为multiline_log时有效 43 | beginning_regex?: string; 44 | // 提取的每个字段的 key 名字,为空的 key 代表丢弃这个字段,只有log_type为delimiter_log时有效,json_log的日志使用 json 本身的 key 45 | keys?: string[]; 46 | // 需要过滤日志的 key,最多5个 47 | filter_keys?: string[]; 48 | // 上述字段 filter_keys 对应的值,个数与 filter_keys 相同,一一对应,采集匹配的日志 49 | filter_regex?: string[]; 50 | } 51 | 52 | export interface CreateTopicData { 53 | // 日志主题归属的日志集的 ID 54 | logset_id: string; 55 | // 日志主题的名字 56 | topic_name: string; 57 | // 主题分区 partition个数,不传参默认创建1个,最大创建允许10个,分裂/合并操作会改变分区数量,整体上限50个 58 | partition_count?: number; 59 | // 旧版日志主题需要采集的日志路径,不采集无需设置 60 | path?: string; 61 | // 新版通配符日志采集路径,以/**/分隔文件目录和文件名,和旧版path只会存在一个 62 | wild_path?: string; 63 | // 采集的日志类型,json_log代表 json 格式日志,delimiter_log代表分隔符格式日志,minimalist_log代表单行全文格式,multiline_log代表多行日志,fullregex_log代表完整正则,默认为minimalist_log 64 | log_type?: string; 65 | // JsonObject 提取规则,如果设置了 extract_rule,则必须设置 log_type 66 | extract_rule?: TopicExtractRule; 67 | } 68 | 69 | export interface UpdateTopicData extends CreateTopicData { 70 | topic_id: string; 71 | } 72 | 73 | export interface IndexFullTextRule { 74 | // 是否大小写敏感 75 | case_sensitive: boolean; 76 | // 全文索引的分词符,不允许为空,建议设置为!@#%^&*()-_="', <>/?|\;:\n\t\r[]{} 77 | tokenizer?: string; 78 | } 79 | export interface IndexKeyValueRule { 80 | // bool 是 是否大小写敏感 81 | case_sensitive: boolean; 82 | // 需要建索引的 key 的名字 83 | keys: string[]; 84 | // 需要建索引 的 key 对应的类型,一一对应,目前支持long double text 85 | types: string[]; 86 | // 上面 key 对应的分词符,一一对应,只对text类型设置,其他类型为空字符串 87 | tokenizers?: string[]; 88 | } 89 | export interface IndexRule { 90 | // 全文索引的相关配置 91 | full_text?: IndexFullTextRule; 92 | // kv 索引的相关配置 93 | key_value?: IndexKeyValueRule; 94 | } 95 | 96 | export interface UpdateIndexData { 97 | // 修改的 index 属于的 topic ID 98 | topic_id: string; 99 | // index 的开关状态 100 | effective: boolean; 101 | // 索引规则,当 effective 为 true 时必需 102 | rule?: IndexRule; 103 | } 104 | 105 | export interface SearchLogData { 106 | // 要查询的 logset ID 107 | logset_id: string; 108 | 109 | // 要查询的 topic ID 110 | topic_ids: string; 111 | 112 | // 要查询的日志的起始时间,格式 YYYY-mm-dd HH:MM:SS 113 | start_time: string; 114 | 115 | // 要查询的日志的结束时间,格式 YYYY-mm-dd HH:MM:SS 116 | end_time: string; 117 | 118 | // 查询语句,详情参考 检索语法与规则 119 | query_string: string; 120 | 121 | // 单次要返回的日志条数,单次返回的最大条数为100 122 | limit: number; 123 | 124 | // 加载更多使用,透传上次返回的 context 值,获取后续的日志内容,通过游标最多可获取10000条,请尽可能缩小时间范围 125 | context?: string; 126 | 127 | // 按时间排序 asc(升序)或者 desc(降序),默认为 desc 128 | sort?: string; 129 | } 130 | -------------------------------------------------------------------------------- /packages/cls/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | KeyValueObject, 3 | sortObjectKey, 4 | stringifyObject, 5 | sha1, 6 | hash, 7 | } from '@tencent-sdk/common'; 8 | 9 | function sortHeaderKey(obj: KeyValueObject): string[] { 10 | const list: string[] = []; 11 | Object.keys(obj).forEach((key: string) => { 12 | const lowerKey = key.toLowerCase(); 13 | if ( 14 | obj.hasOwnProperty(key) && 15 | (lowerKey === 'content-type' || 16 | lowerKey === 'content-md5' || 17 | lowerKey === 'host' || 18 | lowerKey[0] === 'x') 19 | ) { 20 | list.push(key); 21 | } 22 | }); 23 | return list.sort((a: string, b: string) => { 24 | a = a.toLowerCase(); 25 | b = b.toLowerCase(); 26 | return a === b ? 0 : a > b ? 1 : -1; 27 | }); 28 | } 29 | 30 | export interface GenerateSignatureOptions { 31 | secretId: string; 32 | secretKey: string; 33 | method: string; 34 | path: string; 35 | parameters: KeyValueObject; 36 | headers: KeyValueObject; 37 | expire: number; 38 | } 39 | 40 | export function tencentSign({ 41 | secretId, 42 | secretKey, 43 | method, 44 | path, 45 | parameters, 46 | headers, 47 | expire, 48 | }: GenerateSignatureOptions): string { 49 | let now = Math.floor(Date.now() / 1000); 50 | const exp = now + Math.floor(expire / 1000); 51 | now = now - 60; 52 | 53 | // api only support sha1 54 | const ALGORITHM = 'sha1'; 55 | 56 | const signTime = now + ';' + exp; 57 | const sortedHeader = sortHeaderKey(headers) 58 | .join(';') 59 | .toLowerCase(); 60 | 61 | const sortedParameters = sortObjectKey(parameters) 62 | .join(';') 63 | .toLowerCase(); 64 | 65 | // Refer to: https://cloud.tencent.com/document/product/614/12445 66 | // 1. SignKey 67 | const signKey = sha1(signTime, secretKey); 68 | 69 | // 2. HttpRequestInfo 70 | const formatString = [ 71 | method.toLowerCase(), 72 | path, 73 | stringifyObject(parameters, sortObjectKey), 74 | stringifyObject(headers, sortHeaderKey), 75 | '', 76 | ].join('\n'); 77 | //formatString = Buffer.from(formatString, 'utf8'); 78 | 79 | // 3. StringToSign 80 | const stringToSign = [ 81 | ALGORITHM, 82 | signTime, 83 | hash(formatString, ALGORITHM), 84 | '', 85 | ].join('\n'); 86 | 87 | // 4. Signature 88 | const signature = sha1(stringToSign, signKey); 89 | 90 | // 步骤五:构造 Authorization 91 | const authorization = [ 92 | 'q-sign-algorithm=' + ALGORITHM, 93 | 'q-ak=' + secretId, 94 | 'q-sign-time=' + signTime, 95 | 'q-key-time=' + signTime, 96 | 'q-header-list=' + sortedHeader, 97 | 'q-url-param-list=' + sortedParameters, 98 | 'q-signature=' + signature, 99 | ].join('&'); 100 | 101 | return authorization; 102 | } 103 | -------------------------------------------------------------------------------- /packages/cls/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "../../typings"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/common/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.map 3 | tsconfig.tsbuildinfo 4 | test 5 | tests 6 | __test__ 7 | __tests__ 8 | src 9 | tsconfig.json 10 | -------------------------------------------------------------------------------- /packages/common/README.md: -------------------------------------------------------------------------------- 1 | ## @tencent-sdk/common 2 | 3 | Common utils for Tencent SDK 4 | -------------------------------------------------------------------------------- /packages/common/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | deepClone, 3 | getRealType, 4 | isArray, 5 | isObject, 6 | isEmpty, 7 | cleanEmptyValue, 8 | camelCaseProps, 9 | pascalCaseProps, 10 | getUnixTime, 11 | getDate, 12 | flatten, 13 | querystring, 14 | sortObjectKey, 15 | stringifyObject, 16 | sha1, 17 | hash, 18 | } from '../src'; 19 | 20 | describe('Common methods', () => { 21 | const testObj = { 22 | name: 'test', 23 | detail: { 24 | site: 'test.com', 25 | }, 26 | }; 27 | test('deepClone', async () => { 28 | expect(deepClone(testObj)).toEqual({ 29 | name: 'test', 30 | detail: { 31 | site: 'test.com', 32 | }, 33 | }); 34 | }); 35 | test('getRealType', async () => { 36 | expect(getRealType(testObj)).toBe('Object'); 37 | expect(getRealType([])).toBe('Array'); 38 | expect(getRealType({})).toBe('Object'); 39 | expect(getRealType('hello')).toBe('String'); 40 | expect(getRealType(true)).toBe('Boolean'); 41 | expect(getRealType(1)).toBe('Number'); 42 | expect(getRealType(NaN)).toBe('Number'); 43 | }); 44 | test('isArray', async () => { 45 | expect(isArray(testObj)).toBe(false); 46 | expect(isArray([])).toBe(true); 47 | }); 48 | test('isObject', async () => { 49 | expect(isObject(testObj)).toBe(true); 50 | expect(isObject({})).toBe(true); 51 | expect(isObject([1])).toBe(false); 52 | }); 53 | test('isEmpty', async () => { 54 | expect(isEmpty(testObj)).toBe(false); 55 | expect(isEmpty({})).toBe(false); 56 | expect(isEmpty([])).toBe(false); 57 | expect(isEmpty(0)).toBe(false); 58 | expect(isEmpty('')).toBe(false); 59 | expect(isEmpty(false)).toBe(false); 60 | expect(isEmpty(undefined)).toBe(true); 61 | expect(isEmpty(null)).toBe(true); 62 | expect(isEmpty(NaN)).toBe(true); 63 | }); 64 | test('cleanEmptyValue', async () => { 65 | expect( 66 | cleanEmptyValue({ 67 | name: 'test', 68 | isAdult: false, 69 | age: NaN, 70 | children: null, 71 | detail: { 72 | site: undefined, 73 | view: 0, 74 | }, 75 | }), 76 | ).toEqual({ 77 | name: 'test', 78 | isAdult: false, 79 | detail: { 80 | view: 0, 81 | }, 82 | }); 83 | }); 84 | test('pascalCaseProps', async () => { 85 | expect(pascalCaseProps(testObj)).toEqual({ 86 | Name: 'test', 87 | Detail: { 88 | Site: 'test.com', 89 | }, 90 | }); 91 | }); 92 | test('camelCaseProps', async () => { 93 | expect( 94 | camelCaseProps({ 95 | Name: 'test', 96 | Detail: { 97 | Site: 'test.com', 98 | }, 99 | }), 100 | ).toEqual(testObj); 101 | }); 102 | test('getUnixTime', async () => { 103 | expect(getUnixTime(new Date())).toBeGreaterThan(1625079323); 104 | }); 105 | test('getDate', async () => { 106 | const res = getDate(new Date()); 107 | expect(/^(\d){4}-\d{2}-\d{2}$/.test(res)).toBe(true); 108 | }); 109 | test('flatten', async () => { 110 | expect(flatten(testObj)).toEqual({ 111 | name: 'test', 112 | 'detail.site': 'test.com', 113 | }); 114 | }); 115 | test('querystring', async () => { 116 | expect(querystring(flatten(testObj))).toEqual( 117 | 'name=test&detail.site=test.com', 118 | ); 119 | }); 120 | test('sortObjectKey', async () => { 121 | expect(sortObjectKey(testObj)).toEqual(['detail', 'name']); 122 | }); 123 | test('stringifyObject', async () => { 124 | expect(stringifyObject(flatten(testObj), sortObjectKey)).toEqual( 125 | 'detail.site=test.com&name=test', 126 | ); 127 | }); 128 | test('sha1', async () => { 129 | expect(sha1('test', '123')).toEqual( 130 | 'cfa54b5a91f6667966fc8a33362128a4715572f7', 131 | ); 132 | }); 133 | test('hash', async () => { 134 | expect(hash('test', 'sha1')).toEqual( 135 | 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3', 136 | ); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tencent-sdk/common", 3 | "version": "1.0.0", 4 | "description": "Tencent cloud common sdk", 5 | "main": "dist/index.js", 6 | "node": "dist/index.js", 7 | "browser": "dist/index.js", 8 | "module": "dist/index.esm.js", 9 | "jsnext:main": "dist/index.esm.js", 10 | "types": "dist/index.d.ts", 11 | "typings": "dist/index.d.ts", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "scripts": { 16 | "test": "ts-node test/index.test.ts", 17 | "clean": "rimraf ./dist tsconfig.tsbuildinfo" 18 | }, 19 | "keywords": [ 20 | "tencent-cloud", 21 | "common" 22 | ], 23 | "author": "yugasun", 24 | "license": "MIT", 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/yugasun/tencent-sdk.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/yugasun/tencent-sdk/issues" 31 | }, 32 | "homepage": "https://github.com/yugasun/tencent-sdk#readme", 33 | "dependencies": { 34 | "camelcase": "^6.2.0", 35 | "type-fest": "^1.2.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/common/src/cropto.ts: -------------------------------------------------------------------------------- 1 | import { createHmac, createHash, BinaryLike } from 'crypto'; 2 | 3 | export function sha1(str: string, key: BinaryLike): string { 4 | return createHmac('sha1', key) 5 | .update(str) 6 | .digest('hex'); 7 | } 8 | 9 | export function hash(str: string, algorithm: string): string { 10 | return createHash(algorithm) 11 | .update(str) 12 | .digest('hex'); 13 | } 14 | -------------------------------------------------------------------------------- /packages/common/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PascalCasedPropertiesDeep, CamelCasedPropertiesDeep } from 'type-fest'; 2 | import camelCase from 'camelcase'; 3 | 4 | export * from './logger'; 5 | export * from './cropto'; 6 | export interface KeyValueObject { 7 | [key: string]: any; 8 | } 9 | 10 | /** 11 | * simple deep clone object 12 | * @param {object} obj object 13 | */ 14 | export function deepClone(obj: T): T { 15 | return JSON.parse(JSON.stringify(obj)); 16 | } 17 | 18 | /** 19 | * return variable real type 20 | * @param {any} obj input variable 21 | */ 22 | export function getRealType(obj: T): string { 23 | return Object.prototype.toString.call(obj).slice(8, -1); 24 | } 25 | 26 | /** 27 | * is array 28 | * @param obj object 29 | */ 30 | export function isArray(obj: T[] | T): obj is T[] { 31 | return getRealType(obj) === 'Array'; 32 | } 33 | 34 | /** 35 | * is object 36 | * @param obj object 37 | */ 38 | export function isObject(obj: T): obj is T { 39 | return getRealType(obj) === 'Object'; 40 | } 41 | 42 | export function isEmpty(val: T) { 43 | return ( 44 | val === undefined || val === null || (typeof val === 'number' && isNaN(val)) 45 | ); 46 | } 47 | 48 | export function cleanEmptyValue(obj: T): T { 49 | const newObj: any = {}; 50 | for (const key in obj) { 51 | const val = obj[key]; 52 | if (!isEmpty(val)) { 53 | newObj[key] = val; 54 | } 55 | } 56 | return newObj; 57 | } 58 | 59 | export function camelCaseProps( 60 | obj: T, 61 | pascalCase = false, 62 | ): CamelCasedPropertiesDeep | PascalCasedPropertiesDeep { 63 | let res: Record = {}; 64 | if (isObject(obj)) { 65 | res = {} as any; 66 | Object.keys(obj).forEach((key: string) => { 67 | const val = (obj as any)[key]; 68 | const k = camelCase(key, { pascalCase: pascalCase }); 69 | res[k] = 70 | isObject(val) || isArray(val) ? camelCaseProps(val, pascalCase) : val; 71 | }); 72 | } 73 | if (isArray(obj as any)) { 74 | res = []; 75 | (obj as any).forEach((item: any) => { 76 | res.push( 77 | isObject(item) || isArray(item) 78 | ? camelCaseProps(item, pascalCase) 79 | : item, 80 | ); 81 | }); 82 | } 83 | return res as CamelCasedPropertiesDeep; 84 | } 85 | 86 | export function pascalCaseProps(obj: T): PascalCasedPropertiesDeep { 87 | return camelCaseProps(obj, true) as PascalCasedPropertiesDeep; 88 | } 89 | 90 | export function logger(topic: string, content: string): void { 91 | console.log(`[DEBUG] ${topic}: ${content} `); 92 | } 93 | 94 | export function getUnixTime(date: Date) { 95 | const val = date.getTime(); 96 | return Math.ceil(val / 1000); 97 | } 98 | 99 | export function getDate(date: Date) { 100 | const year = date.getUTCFullYear(); 101 | const month = date.getUTCMonth() + 1; 102 | const day = date.getUTCDate(); 103 | return `${year}-${month > 9 ? month : `0${month}`}-${ 104 | day > 9 ? day : `0${day}` 105 | }`; 106 | } 107 | 108 | /** 109 | * iterate object or array 110 | * @param obj object or array 111 | * @param iterator iterator function 112 | */ 113 | export function forEach( 114 | obj: object | any[], 115 | iterator: (value: any, index: number | string, array: any) => void, 116 | ) { 117 | if (isArray(obj)) { 118 | let arr = obj as Array; 119 | if (arr.forEach) { 120 | arr.forEach(iterator); 121 | return; 122 | } 123 | for (let i = 0; i < arr.length; i += 1) { 124 | iterator(arr[i], i, arr); 125 | } 126 | } else { 127 | const oo = obj as { [propName: string]: any }; 128 | for (let key in oo) { 129 | if (obj.hasOwnProperty(key)) { 130 | iterator(oo[key], key, obj); 131 | } 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * flatter request parameter 138 | * @param obj target object or array 139 | */ 140 | export function flatten(obj: { 141 | [propName: string]: any; 142 | }): { [propName: string]: any } { 143 | if (!isArray(obj) && !isObject(obj)) { 144 | return {}; 145 | } 146 | const ret: { [propName: string]: any } = {}; 147 | const dump = function( 148 | obj: object | Array, 149 | prefix: string | null, 150 | parents?: any[], 151 | ) { 152 | const checkedParents: any[] = []; 153 | if (parents) { 154 | let i; 155 | for (i = 0; i < parents.length; i++) { 156 | if (parents[i] === obj) { 157 | throw new Error('object has circular references'); 158 | } 159 | checkedParents.push(obj); 160 | } 161 | } 162 | checkedParents.push(obj); 163 | if (!isArray(obj) && !isObject(obj)) { 164 | if (!prefix) { 165 | throw obj + 'is not object or array'; 166 | } 167 | ret[prefix] = obj; 168 | return {}; 169 | } 170 | 171 | if (isArray(obj)) { 172 | // it's an array 173 | forEach(obj, function(obj, i) { 174 | dump(obj, prefix ? prefix + '.' + i : '' + i, checkedParents); 175 | }); 176 | } else { 177 | // it's an object 178 | forEach(obj, function(obj, key) { 179 | dump(obj, prefix ? prefix + '.' + key : '' + key, checkedParents); 180 | }); 181 | } 182 | }; 183 | 184 | dump(obj, null); 185 | return ret; 186 | } 187 | 188 | function stringifyPrimitive(v: any) { 189 | switch (typeof v) { 190 | case 'string': 191 | return v; 192 | 193 | case 'boolean': 194 | return v ? 'true' : 'false'; 195 | 196 | case 'number': 197 | return isFinite(v) ? v : ''; 198 | 199 | default: 200 | return ''; 201 | } 202 | } 203 | function safeUrlEncode(str: string | number | boolean) { 204 | return encodeURIComponent(str) 205 | .replace(/!/g, '%21') 206 | .replace(/'/g, '%27') 207 | .replace(/\(/g, '%28') 208 | .replace(/\)/g, '%29') 209 | .replace(/\*/g, '%2A'); 210 | } 211 | 212 | export function querystring( 213 | obj?: KeyValueObject, 214 | isSafeEncode: boolean = false, 215 | ): string { 216 | const sep = '&'; 217 | const eq = '='; 218 | const encodeHandler = isSafeEncode ? safeUrlEncode : encodeURIComponent; 219 | if (!obj) return ''; 220 | 221 | if (obj && typeof obj === 'object') { 222 | return Object.keys(obj) 223 | .map(function(k) { 224 | let ks = encodeHandler(stringifyPrimitive(k)) + eq; 225 | if (Array.isArray(obj[k])) { 226 | return (obj[k] as Array) 227 | .map(function(v) { 228 | return ks + encodeHandler(stringifyPrimitive(v)); 229 | }) 230 | .join(sep); 231 | } else { 232 | return ks + encodeHandler(stringifyPrimitive(obj[k])); 233 | } 234 | }) 235 | .filter(Boolean) 236 | .join(sep); 237 | } 238 | 239 | return ''; 240 | } 241 | 242 | export function sortObjectKey(obj: KeyValueObject): string[] { 243 | const list: string[] = []; 244 | Object.keys(obj).forEach((key: string) => { 245 | if (obj.hasOwnProperty(key)) { 246 | list.push(key); 247 | } 248 | }); 249 | 250 | return list.sort((a: string, b: string) => { 251 | a = a.toLowerCase(); 252 | b = b.toLowerCase(); 253 | return a === b ? 0 : a > b ? 1 : -1; 254 | }); 255 | } 256 | 257 | export function stringifyObject( 258 | obj: KeyValueObject, 259 | sortKeyMethod: (o: KeyValueObject) => string[], 260 | ): string { 261 | const list: string[] = []; 262 | const keyList = sortKeyMethod(obj); 263 | keyList.forEach((key) => { 264 | let val = obj[key] === undefined || obj[key] === null ? '' : '' + obj[key]; 265 | key = key.toLowerCase(); 266 | key = safeUrlEncode(key); 267 | val = safeUrlEncode(val) || ''; 268 | list.push(key + '=' + val); 269 | }); 270 | return list.join('&'); 271 | } 272 | -------------------------------------------------------------------------------- /packages/common/src/logger.ts: -------------------------------------------------------------------------------- 1 | export function logger(topic: string, content: string): void { 2 | console.log(`[DEBUG] ${topic}: ${content} `); 3 | } 4 | -------------------------------------------------------------------------------- /packages/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "../../typings"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/faas/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.map 3 | tsconfig.tsbuildinfo 4 | test 5 | tests 6 | __test__ 7 | __tests__ 8 | src 9 | tsconfig.json 10 | -------------------------------------------------------------------------------- /packages/faas/README.md: -------------------------------------------------------------------------------- 1 | ## Tencent Cloud FaaS SDK 2 | 3 | 专门为 [腾讯云云函数](https://console.cloud.tencent.com/scf) 提供的 SDK 工具. 4 | 5 | ## 安装 6 | 7 | ```bash 8 | $ npm i @tencent-sdk/faas --save 9 | # 或者 10 | $ yarn add @tencent-sdk/faas 11 | ``` 12 | 13 | ## 使用 14 | 15 | 初始化实例: 16 | 17 | ```js 18 | import { FaaS } from '@tencent-sdk/faas'; 19 | 20 | const client = new FaaS({ 21 | secretId: 'Please input your SecretId', 22 | secretKey: 'Please input your SecretKey', 23 | token: 'Please input your Token', 24 | region: 'ap-guangzhou', 25 | debug: false, 26 | }); 27 | ``` 28 | 29 | ### 参数说明 30 | 31 | | 参数 | 描述 | 类型 | 必须 | 默认值 | 32 | | --------- | ------------------------ | ------- | :--: | ------------ | 33 | | secretId | 腾讯云 API 密钥 ID | string | 是 | '' | 34 | | secretKey | 腾讯云 API 密钥 Key | string | 是 | '' | 35 | | token | 腾讯云临时鉴权密钥 Token | string | 否 | '' | 36 | | region | 请求服务地域 | string | 否 | ap-guangzhou | 37 | | debug | 是否打印调试信息 | boolean | 否 | false | 38 | 39 | ### 支持方法 40 | 41 | - 获取地区配置 [getRegion()](#getRegion) 42 | - 设置地区 [setRegion()](#setRegion) 43 | - 调用该函数 [invoke()](#invoke) 44 | - 获取函数 CLS 配置 [getClsConfig()](#getClsConfig) 45 | - 获取 CLS 日志列表 [getLogList()](#getLogList) 46 | - 获取日志详情 [getLogDetail()](#getLogDetail) 47 | - 通过请求 ID 获取日志详情 [getLogByReqId()](#getLogByReqId) 48 | 49 | ### getRegion 50 | 51 | 获取当前地区配置: 52 | 53 | ```js 54 | const region = client.getRegion(); 55 | ``` 56 | 57 | ### setRegion 58 | 59 | 配置服务地区: 60 | 61 | ```js 62 | client.setRegion('ap-guangzhou'); 63 | ``` 64 | 65 | ### invoke 66 | 67 | 调用函数: 68 | 69 | ```js 70 | const res = await faas.invoke({ 71 | name: 'serverless-test', 72 | namespace: 'default', 73 | qualifier: '$LATEST', 74 | }); 75 | ``` 76 | 77 | 参数说明: 78 | 79 | | 参数 | 描述 | 类型 | 必须 | 默认 | 80 | | --------- | -------- | :----: | :--: | --------- | 81 | | name | 函数名称 | string | 是 | | 82 | | namespace | 命名空间 | string | 否 | `default` | 83 | | qualifier | 函数版本 | string | 否 | `$LATEST` | 84 | | event | 触发参数 | object | 否 | `{}` | 85 | 86 | `event` 为触发函数的事件对象的。 87 | 88 | 如果函数是 `web` 类型,`event` 对象参数如下: 89 | 90 | | 参数 | 描述 | 类型 | 必须 | 默认 | 91 | | ------ | -------- | :----: | :--: | ----- | 92 | | method | 请求方法 | string | 否 | `get` | 93 | | path | 请求路径 | string | 否 | `/` | 94 | | data | 请求数据 | object | 否 | `{}` | 95 | 96 | ### getClsConfig 97 | 98 | 获取函数 CLS 配置: 99 | 100 | ```js 101 | const res = await faas.getClsConfig({ 102 | name: 'serverless-test', 103 | namespace: 'default', 104 | qualifier: '$LATEST', 105 | }); 106 | ``` 107 | 108 | ### getLogList 109 | 110 | 获取日志列表: 111 | 112 | ```js 113 | const res = await faas.getLogList({ 114 | name: 'serverless-test', 115 | namespace: 'default', 116 | qualifier: '$LATEST', 117 | }); 118 | ``` 119 | 120 | > 注意: 默认获取最近 10 分钟日志。 121 | 122 | 通过 `startTime` 和 `endTime` 参数,获取时间段内日志: 123 | 124 | ```js 125 | const res = await faas.getLogList({ 126 | name: 'serverless-test', 127 | namespace: 'default', 128 | qualifier: '$LATEST', 129 | startTime: '2021-04-30 14:00:00', 130 | endTime: '2021-04-30 14:15:00', 131 | }); 132 | ``` 133 | 134 | > 注意:时间必须是 UTC+8 (亚洲/上海时区)时间。 135 | 136 | 由于云函数是流失日志,日志落盘到 CLS 是有时间延迟的,所以在实时获取日志是会存在最新的部分日志信息并不完整,如果需要过滤掉这些不完整的日志,可以通过传递参数 `isFilterCompleted` 为 `true` 来实现。 137 | 138 | 参数说明: 139 | 140 | | 参数 | 描述 | 类型 | 必须 | 默认 | 141 | | ----------------- | ---------------------------------- | :--------------: | :--: | ------------ | 142 | | name | 函数名称 | string | 是 | | 143 | | namespace | 命名空间 | string | 否 | `default` | 144 | | qualifier | 函数版本 | string | 否 | `$LATEST` | 145 | | startTime | 开始时间,支持格式化的时间和时间戳 | `string\|number` | 否 | | 146 | | endTime | 结束时间,支持格式化的时间和时间戳 | `string\|number` | 否 | `Date.now()` | 147 | | reqId | 请求 ID | string | 否 | | 148 | | status | 日志状态 | string | 否 | | 149 | | interval | 时间间隔,单位秒 | string | 否 | `600s` | 150 | | limit | 获取条数 | string | 否 | | 151 | | isFilterCompleted | 是否过滤掉不完整的日志 | boolean | 否 | `false` | 152 | 153 | ### getLogDetail 154 | 155 | 获取指定请求 ID 日志详情(日志元数据): 156 | 157 | ```js 158 | const res = await faas.getLogDetail({ 159 | name: 'serverless-test', 160 | namespace: 'default', 161 | qualifier: '$LATEST', 162 | logsetId: 'xxx-xxx', 163 | topicId: 'xxx-xxx', 164 | reqId: 'xxx-xxx', 165 | }); 166 | ``` 167 | 168 | 参数说明: 169 | 170 | | 参数 | 描述 | 类型 | 必须 | 默认 | 171 | | --------- | ----------- | :----: | :--: | --------- | 172 | | name | 函数名称 | string | 是 | | 173 | | namespace | 命名空间 | string | 否 | `default` | 174 | | qualifier | 函数版本 | string | 否 | `$LATEST` | 175 | | logsetId | 日志集 ID | string | 是 | | 176 | | topicId | 日志主题 ID | string | 是 | | 177 | | reqId | 请求 ID | string | 是 | | 178 | 179 | ### getLogByReqId 180 | 181 | 通过请求 ID 获取日志详情(组装日志数据): 182 | 183 | ```js 184 | const res = await faas.getLogByReqId({ 185 | name: 'serverless-test', 186 | namespace: 'default', 187 | qualifier: '$LATEST', 188 | reqId: 'xxx-xxx', 189 | }); 190 | ``` 191 | 192 | 参数说明: 193 | 194 | | 参数 | 描述 | 类型 | 必须 | 默认 | 195 | | --------- | -------- | :----: | :--: | --------- | 196 | | name | 函数名称 | string | 是 | | 197 | | namespace | 命名空间 | string | 否 | `default` | 198 | | qualifier | 函数版本 | string | 否 | `$LATEST` | 199 | | reqId | 请求 ID | string | 是 | | 200 | 201 | ## 错误码 202 | 203 | | 类型 | 错误码 | 描述 | 204 | | ---------------------- | ------ | ------------------------ | 205 | | API_FAAS_get | 1000 | 查找函数其他错误信息 | 206 | | API_FAAS_get | 1001 | 无法找到制定函数 | 207 | | API_FAAS_getClsConfig | 1002 | 无法获取函数的 CLS 配置 | 208 | | API_FAAS_getLogByReqId | 1003 | 无效的请求 ID | 209 | | API_FAAS_getTriggers | 1004 | 无法获取函数的触发器列表 | 210 | | API_FAAS_getNamespace | 1005 | 未找到制定的 namespace | 211 | | API_FAAS_getQualifier | 1006 | 未找到指定的 qualifier | 212 | 213 | ## License 214 | 215 | MIT 216 | -------------------------------------------------------------------------------- /packages/faas/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { FaaS, InvokeResult } from '../src'; 2 | 3 | describe('FaaS', () => { 4 | const region = 'ap-guangzhou'; 5 | const faasConfig = { 6 | name: 'serverless-test', 7 | namespace: 'default', 8 | qualifier: '$LATEST', 9 | }; 10 | const clsConfig = { 11 | logsetId: '750b324e-f97a-40e8-9b73-31475c37c02a', 12 | topicId: '34e08a87-95b0-4f8d-85c7-a823c5f630e9', 13 | }; 14 | let reqId = ''; 15 | const faas = new FaaS({ 16 | debug: !!process.env.DEBUG, 17 | secretId: process.env.TENCENT_SECRET_ID, 18 | secretKey: process.env.TENCENT_SECRET_KEY, 19 | token: process.env.TENCENT_TOKEN, 20 | region, 21 | }); 22 | 23 | describe('base methods', () => { 24 | test('getRegion', async () => { 25 | const region = faas.getRegion(); 26 | 27 | expect(region).toBe(region); 28 | }); 29 | 30 | test('setRegion', async () => { 31 | faas.setRegion('ap-shanghai'); 32 | 33 | expect(faas.getRegion()).toBe('ap-shanghai'); 34 | 35 | // 还原为 ap-guangzhou 36 | faas.setRegion(region); 37 | }); 38 | 39 | test('getNamespaces', async () => { 40 | const res = await faas.getNamespaces(); 41 | expect(Array.isArray(res)).toBe(true); 42 | }); 43 | 44 | test('getVersions', async () => { 45 | const res = await faas.getVersions({ 46 | ...faasConfig, 47 | }); 48 | 49 | expect(Array.isArray(res)).toBe(true); 50 | }); 51 | }); 52 | 53 | describe('invoke', () => { 54 | test('invoke', async () => { 55 | const res = (await faas.invoke({ 56 | ...faasConfig, 57 | })) as InvokeResult; 58 | 59 | expect(res).toEqual({ 60 | billDuration: expect.any(Number), 61 | duration: expect.any(Number), 62 | errMsg: expect.any(String), 63 | memUsage: expect.any(Number), 64 | functionRequestId: expect.any(String), 65 | invokeResult: expect.any(Number), 66 | log: expect.any(String), 67 | retMsg: expect.any(String), 68 | }); 69 | 70 | reqId = res.functionRequestId; 71 | }); 72 | 73 | test('invoke with qualifier $DEFAULT', async () => { 74 | const res = (await faas.invoke({ 75 | ...faasConfig, 76 | qualifier: '$DEFAULT', 77 | })) as InvokeResult; 78 | 79 | expect(res).toEqual({ 80 | billDuration: expect.any(Number), 81 | duration: expect.any(Number), 82 | errMsg: expect.any(String), 83 | memUsage: expect.any(Number), 84 | functionRequestId: expect.any(String), 85 | invokeResult: expect.any(Number), 86 | log: expect.any(String), 87 | retMsg: expect.any(String), 88 | }); 89 | }); 90 | 91 | test('invoke with wrong qualifier', async () => { 92 | try { 93 | await faas.invoke({ 94 | ...faasConfig, 95 | qualifier: 'wrong_qualifier', 96 | }); 97 | } catch (e) { 98 | expect(e.code).toBe('1006'); 99 | } 100 | }); 101 | 102 | test('invoke with wrong region', async () => { 103 | try { 104 | faas.setRegion('ap-test'); 105 | await faas.invoke({ 106 | ...faasConfig, 107 | }); 108 | } catch (e) { 109 | expect(e.code).toBe('1001'); 110 | } 111 | faas.setRegion(region); 112 | }); 113 | test('invoke with wrong namespace', async () => { 114 | try { 115 | await faas.invoke({ 116 | ...faasConfig, 117 | namespace: 'not_exist_namespace', 118 | }); 119 | } catch (e) { 120 | expect(e.code).toBe('1005'); 121 | } 122 | }); 123 | test('invoke with wrong qualifier', async () => { 124 | try { 125 | await faas.invoke({ 126 | ...faasConfig, 127 | qualifier: 'not_exist_qualifier', 128 | }); 129 | } catch (e) { 130 | expect(e.code).toBe('1006'); 131 | } 132 | }); 133 | }); 134 | 135 | describe('log', () => { 136 | test('getClsConfig', async () => { 137 | const res = await faas.getClsConfig({ 138 | ...faasConfig, 139 | }); 140 | 141 | expect(res).toEqual(clsConfig); 142 | }); 143 | 144 | test('getLogList', async () => { 145 | const res = await faas.getLogList({ 146 | ...faasConfig, 147 | }); 148 | 149 | if (res[0]) { 150 | reqId = res[0]!.requestId; 151 | } 152 | 153 | expect(res).toBeInstanceOf(Array); 154 | }); 155 | 156 | test('getLogList isFilterCompleted = true', async () => { 157 | const res = await faas.getLogList({ 158 | ...faasConfig, 159 | isFilterCompleted: true, 160 | }); 161 | 162 | const uncompletedLog = res.filter((item) => !item.isCompleted); 163 | 164 | expect(uncompletedLog.length).toBe(0); 165 | }); 166 | 167 | test('getLogDetail', async () => { 168 | const res = await faas.getLogDetail({ 169 | ...faasConfig, 170 | ...clsConfig, 171 | reqId, 172 | }); 173 | expect(res).toBeInstanceOf(Array); 174 | }); 175 | test('getLogByReqId', async () => { 176 | const res = await faas.getLogByReqId({ 177 | ...faasConfig, 178 | reqId, 179 | }); 180 | 181 | expect(res).toEqual({ 182 | requestId: reqId, 183 | retryNum: 0, 184 | startTime: expect.any(String), 185 | memoryUsage: expect.any(String), 186 | duration: expect.any(String), 187 | message: expect.any(String), 188 | isCompleted: expect.any(Boolean), 189 | }); 190 | }); 191 | }); 192 | 193 | describe('metric', () => { 194 | test('getMetric', async () => { 195 | const res = await faas.getMetric({ 196 | ...faasConfig, 197 | metric: 'Invocation', 198 | isRaw: false, 199 | }); 200 | expect(res).toBeInstanceOf(Array); 201 | if (res.length > 0) { 202 | expect(res).toEqual( 203 | expect.arrayContaining([ 204 | { 205 | time: expect.any(String), 206 | value: expect.any(Number), 207 | timestamp: expect.any(Number), 208 | }, 209 | ]), 210 | ); 211 | } 212 | }); 213 | 214 | test('[isRaw = true] getMetric', async () => { 215 | const res = await faas.getMetric({ 216 | ...faasConfig, 217 | metric: 'Invocation', 218 | isRaw: true, 219 | }); 220 | 221 | expect(res).toEqual({ 222 | StartTime: expect.any(String), 223 | EndTime: expect.any(String), 224 | MetricName: expect.any(String), 225 | Period: expect.any(Number), 226 | DataPoints: expect.arrayContaining([ 227 | { 228 | Dimensions: expect.any(Array), 229 | Timestamps: expect.any(Array), 230 | Values: expect.any(Array), 231 | }, 232 | ]), 233 | RequestId: expect.stringMatching(/.{36}/g), 234 | }); 235 | }); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /packages/faas/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tencent-sdk/faas", 3 | "version": "1.0.9", 4 | "description": "Tencent cloud faas sdk", 5 | "main": "dist/index.js", 6 | "node": "dist/index.js", 7 | "browser": "dist/index.js", 8 | "module": "dist/index.esm.js", 9 | "jsnext:main": "dist/index.esm.js", 10 | "types": "dist/index.d.ts", 11 | "typings": "dist/index.d.ts", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "scripts": { 16 | "test": "ts-node test/index.test.ts", 17 | "clean": "rimraf ./dist tsconfig.tsbuildinfo" 18 | }, 19 | "keywords": [ 20 | "tencent-cloud", 21 | "faas", 22 | "scf" 23 | ], 24 | "author": "yugasun", 25 | "license": "MIT", 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/yugasun/tencent-sdk.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/yugasun/tencent-sdk/issues" 32 | }, 33 | "homepage": "https://github.com/yugasun/tencent-sdk#readme", 34 | "dependencies": { 35 | "@tencent-sdk/capi": "2.0.3", 36 | "@tencent-sdk/cls": "1.0.1", 37 | "@tencent-sdk/common": "1.0.0", 38 | "dayjs": "^1.10.4", 39 | "got": "^11.8.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/faas/src/apis.ts: -------------------------------------------------------------------------------- 1 | import { Capi, ServiceType, ApiFactory } from '@tencent-sdk/capi'; 2 | 3 | const ACTIONS = [ 4 | 'CreateFunction', 5 | 'DeleteFunction', 6 | 'GetFunction', 7 | 'UpdateFunctionCode', 8 | 'UpdateFunctionConfiguration', 9 | 'CreateTrigger', 10 | 'DeleteTrigger', 11 | 'PublishVersion', 12 | 'ListAliases', 13 | 'CreateAlias', 14 | 'UpdateAlias', 15 | 'DeleteAlias', 16 | 'GetAlias', 17 | 'Invoke', 18 | 'ListTriggers', 19 | 'ListNamespaces', 20 | 'ListVersionByFunction', 21 | 'ListAliases', 22 | ] as const; 23 | 24 | export type ActionType = typeof ACTIONS[number]; 25 | 26 | export type ApiMap = Record any>; 27 | 28 | function initializeApis({ 29 | isV3 = true, 30 | debug = false, 31 | }: { 32 | isV3?: boolean; 33 | debug?: boolean; 34 | }): ApiMap { 35 | return ApiFactory({ 36 | isV3, 37 | debug, 38 | serviceType: ServiceType.faas, 39 | version: '2018-04-16', 40 | actions: ACTIONS, 41 | errorHandler: (e) => { 42 | console.log(e); 43 | }, 44 | }); 45 | } 46 | 47 | export { initializeApis }; 48 | -------------------------------------------------------------------------------- /packages/faas/src/constants.ts: -------------------------------------------------------------------------------- 1 | const ERRORS = { 2 | OTHER_GET_FAAS_ERROR: { 3 | type: 'API_FAAS_get', 4 | code: `1000`, 5 | }, 6 | GET_FAAS_ERROR: { 7 | type: 'API_FAAS_get', 8 | code: `1001`, 9 | message: `[FAAS] 无法找到指定函数,请部署后调用或检查函数名称`, 10 | }, 11 | GET_CLS_CONFIG_ERROR: { 12 | type: 'API_FAAS_getClsConfig', 13 | code: `1002`, 14 | message: `[FAAS] 无法获取函数 CLS 配置,请检查函数是否配置 CLS 功能`, 15 | }, 16 | REQUEST_ID_INVALID: { 17 | type: 'API_FAAS_getLogByReqId', 18 | code: `1003`, 19 | message: `[FAAS] 参数 reqId(请求 ID) 无效`, 20 | }, 21 | WEB_FAAS_NO_TRIGGERS: { 22 | type: 'API_FAAS_getTriggers', 23 | code: `1004`, 24 | message: `[FAAS] WEB 类型函数必须配置 API 网关触发器才能执行`, 25 | }, 26 | NAMESPACE_NOT_EXIST_ERROR: { 27 | type: 'API_FAAS_namespace', 28 | code: `1005`, 29 | message: '[FAAS] 未找到指定的 namespace,请创建后再试', 30 | }, 31 | 32 | QUALIFIER_NOT_EXIST_ERROR: { 33 | type: 'API_FAAS_qualifier', 34 | code: `1006`, 35 | message: '[FAAS] 未找到指定的 qualifier (版本或者别名)', 36 | }, 37 | }; 38 | 39 | export { ERRORS }; 40 | -------------------------------------------------------------------------------- /packages/faas/src/dayjs.ts: -------------------------------------------------------------------------------- 1 | import dayjs, { Dayjs, ConfigType } from 'dayjs'; 2 | import tz from 'dayjs/plugin/timezone'; 3 | import utc from 'dayjs/plugin/utc'; 4 | 5 | dayjs.extend(tz); 6 | dayjs.extend(utc); 7 | 8 | dayjs.tz.setDefault('Asia/Shanghai'); 9 | 10 | const dtz = (date: ConfigType = Date.now()) => { 11 | return dayjs.tz(date); 12 | }; 13 | 14 | const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; 15 | const TIME_FORMAT_TIMEZONE = 'YYYY-MM-DDTHH:mm:ssZ'; 16 | 17 | function formatDate(date: ConfigType, withTimeout = false): string { 18 | if (typeof date === 'number') { 19 | date = `${date}`.length === 10 ? date * 1000 : date; 20 | } 21 | return dtz(date).format(withTimeout ? TIME_FORMAT_TIMEZONE : TIME_FORMAT); 22 | } 23 | 24 | export { dayjs, dtz, Dayjs, TIME_FORMAT, formatDate }; 25 | -------------------------------------------------------------------------------- /packages/faas/src/index.ts: -------------------------------------------------------------------------------- 1 | import got from 'got'; 2 | import { camelCaseProps } from '@tencent-sdk/common'; 3 | import { Capi, ServiceType, CommonError } from '@tencent-sdk/capi'; 4 | import { Cls } from '@tencent-sdk/cls'; 5 | import { dtz, dayjs, formatDate, Dayjs, TIME_FORMAT } from './dayjs'; 6 | import { initializeApis, ApiMap, ActionType } from './apis'; 7 | import { getSearchSql, formatFaasLog } from './utils'; 8 | import { Monitor } from './monitor'; 9 | import { 10 | Credentials, 11 | FaasOptions, 12 | FaasBaseConfig, 13 | FunctionInfo, 14 | GetFaasOptions, 15 | GetVersionsOptions, 16 | GetLogOptions, 17 | GetLogDetailOptions, 18 | ClsConfig, 19 | SearchLogItem, 20 | SearchLogDetailItem, 21 | InvokeOptions, 22 | InvokeType, 23 | LogType, 24 | InvokeResult, 25 | GetMonitorDataOptions, 26 | MonitorData, 27 | FormatedMonitorData, 28 | GetTriggersOptions, 29 | TriggerData, 30 | GetAliasesOptions, 31 | Version, 32 | Alias, 33 | } from './typings'; 34 | import { ERRORS } from './constants'; 35 | 36 | export * from './typings'; 37 | export * from './monitor'; 38 | 39 | export class FaaS { 40 | credentials: Credentials; 41 | region: string; 42 | debug: boolean; 43 | capi: Capi; 44 | cls: Cls; 45 | monitor: Monitor; 46 | apis: ApiMap; 47 | clsConfigCache: { [prop: string]: { logsetId: string; topicId: string } }; 48 | 49 | constructor({ 50 | secretId, 51 | secretKey, 52 | token, 53 | region = 'ap-guangzhou', 54 | debug = false, 55 | }: FaasOptions) { 56 | this.credentials = { 57 | secretId, 58 | secretKey, 59 | token, 60 | }; 61 | this.debug = debug; 62 | this.region = region; 63 | this.capi = new Capi({ 64 | debug, 65 | region: region, 66 | serviceType: ServiceType.faas, 67 | secretId: secretId, 68 | secretKey: secretKey, 69 | token: token, 70 | }); 71 | 72 | this.cls = new Cls({ 73 | debug, 74 | region: this.region, 75 | secretId: secretId, 76 | secretKey: secretKey, 77 | token: token, 78 | }); 79 | 80 | this.monitor = new Monitor({ 81 | debug, 82 | region: this.region, 83 | secretId: secretId, 84 | secretKey: secretKey, 85 | token: token, 86 | }); 87 | 88 | // 函数 CLS 配置缓存 89 | this.clsConfigCache = {}; 90 | 91 | this.apis = initializeApis({ 92 | debug, 93 | }); 94 | } 95 | 96 | /** 97 | * 设置当前地域 98 | * @param {string} region 地区 99 | */ 100 | setRegion(region: string): void { 101 | this.region = region; 102 | this.cls.options.region = region; 103 | } 104 | 105 | /** 106 | * 获取当前地域 107 | * @returns {string} 地域 108 | */ 109 | getRegion() { 110 | return this.region; 111 | } 112 | 113 | async request({ 114 | Action, 115 | ...data 116 | }: { 117 | Action: ActionType; 118 | [key: string]: any; 119 | }) { 120 | const result = await this.apis[Action](this.capi, data); 121 | return result; 122 | } 123 | 124 | async getTriggers({ 125 | name, 126 | namespace = 'default', 127 | page = 0, 128 | }: GetTriggersOptions): Promise { 129 | const limit = 100; 130 | const { Triggers = [], TotalCount } = await this.request({ 131 | Action: 'ListTriggers', 132 | FunctionName: name, 133 | Namespace: namespace, 134 | Limit: limit, 135 | Offset: page * limit, 136 | }); 137 | if (TotalCount > 100) { 138 | const res = await this.getTriggers({ 139 | name, 140 | namespace, 141 | page: page + 1, 142 | }); 143 | return Triggers.concat(res); 144 | } 145 | 146 | return Triggers; 147 | } 148 | 149 | /** 150 | * 获取 faas 命名空间列表 151 | */ 152 | async getNamespaces(options: { page?: number; limit?: number } = {}) { 153 | const { page = 0, limit = 20 } = options; 154 | let { Namespaces = [] } = await this.request({ 155 | Action: 'ListNamespaces', 156 | Offset: page * limit, 157 | Limit: limit, 158 | }); 159 | if (Namespaces.length >= limit) { 160 | const res = await this.getNamespaces({ 161 | page: page + 1, 162 | limit, 163 | }); 164 | Namespaces = Namespaces.concat(res); 165 | } 166 | 167 | return Namespaces; 168 | } 169 | 170 | /** 171 | * 获取 faas 版本列表 172 | * @param {getVersionsOptions} options 参数 173 | * @returns {Promise} 函数版本列表,字符串数组 174 | */ 175 | async getVersions({ 176 | name, 177 | namespace = 'default', 178 | page = 0, 179 | limit = 20, 180 | order = 'DESC', 181 | orderBy = 'AddTime', 182 | }: GetVersionsOptions): Promise { 183 | let { Versions = [] } = await this.request({ 184 | Action: 'ListVersionByFunction', 185 | Namespace: namespace, 186 | FunctionName: name, 187 | Offset: page * limit, 188 | Limit: limit, 189 | Order: order, 190 | OrderBy: orderBy, 191 | }); 192 | if (Versions.length >= limit) { 193 | const res = await this.getVersions({ 194 | name, 195 | namespace, 196 | page: page + 1, 197 | limit, 198 | order, 199 | orderBy, 200 | }); 201 | Versions = Versions.concat(res); 202 | } 203 | return Versions; 204 | } 205 | 206 | /** 207 | * 获取 faas 别名列表 208 | * @param {GetAliasesOptions} options 参数 209 | * @returns {Promise} 函数版本列表,字符串数组 210 | */ 211 | async getAliases({ 212 | name, 213 | namespace = 'default', 214 | page = 0, 215 | limit = 20, 216 | }: GetAliasesOptions): Promise { 217 | let { Aliases = [] } = await this.request({ 218 | Action: 'ListAliases', 219 | Namespace: namespace, 220 | FunctionName: name, 221 | // FIXME: 云 API 定义 Offset 和 Limit 必须是 string 类型 222 | Offset: `${page * limit}`, 223 | Limit: `${limit}`, 224 | }); 225 | 226 | if (Aliases.length >= limit) { 227 | const res = await this.getAliases({ 228 | name, 229 | namespace, 230 | page: page + 1, 231 | limit, 232 | }); 233 | Aliases = Aliases.concat(res); 234 | } 235 | return Aliases; 236 | } 237 | 238 | async isVersionExist({ 239 | name, 240 | namespace = 'default', 241 | version, 242 | }: { 243 | version: string; 244 | name: string; 245 | namespace?: string; 246 | }) { 247 | const versionList = await this.getVersions({ 248 | name, 249 | namespace, 250 | }); 251 | const versions = versionList.map((item) => item.Version); 252 | 253 | return versions.indexOf(version) > -1; 254 | } 255 | 256 | async isAliasExist({ 257 | name, 258 | namespace = 'default', 259 | alias, 260 | }: { 261 | alias: string; 262 | name: string; 263 | namespace?: string; 264 | }) { 265 | const aliasList = await this.getAliases({ 266 | name, 267 | namespace, 268 | }); 269 | const aliases = aliasList.map((item) => item.Name); 270 | 271 | return aliases.indexOf(alias) > -1; 272 | } 273 | 274 | /** 275 | * 获取 faas 详情 276 | * @param {GetFaasOptions} options 参数 277 | * @returns {Promise} 函数详情,如果不存在则返回 null 278 | */ 279 | async get({ 280 | name, 281 | namespace = 'default', 282 | qualifier = '$LATEST', 283 | showCode = false, 284 | showTriggers = false, 285 | }: GetFaasOptions): Promise { 286 | try { 287 | // 判断 namespace 是否存在 288 | const namespacesList = await this.getNamespaces(); 289 | const namespaces = namespacesList.map( 290 | (item: { Name: string }) => item.Name, 291 | ); 292 | if (namespaces.indexOf(namespace) === -1) { 293 | throw new CommonError(ERRORS.NAMESPACE_NOT_EXIST_ERROR); 294 | } 295 | const versionExist = await this.isVersionExist({ 296 | name, 297 | namespace, 298 | version: qualifier, 299 | }); 300 | 301 | if (!versionExist) { 302 | const aliasExist = await this.isAliasExist({ 303 | name, 304 | namespace, 305 | alias: qualifier, 306 | }); 307 | 308 | if (!aliasExist) { 309 | throw new CommonError(ERRORS.QUALIFIER_NOT_EXIST_ERROR); 310 | } 311 | } 312 | } catch (e) { 313 | // 可能是其他参数导致的,比如 region 不存在,直接返回 null,排除函数不存在异常 314 | if ( 315 | e.code !== ERRORS.NAMESPACE_NOT_EXIST_ERROR.code && 316 | e.code !== ERRORS.QUALIFIER_NOT_EXIST_ERROR.code 317 | ) { 318 | return null; 319 | } 320 | throw e; 321 | } 322 | try { 323 | const Response = await this.request({ 324 | Action: 'GetFunction', 325 | FunctionName: name, 326 | Namespace: namespace, 327 | Qualifier: qualifier, 328 | ShowCode: showCode ? 'TRUE' : 'FALSE', 329 | ShowTriggers: showTriggers ? 'TRUE' : 'FALSE', 330 | }); 331 | return Response; 332 | } catch (e) { 333 | if (e.code === 'ResourceNotFound.Namespace') { 334 | throw new CommonError(ERRORS.NAMESPACE_NOT_EXIST_ERROR); 335 | } 336 | if ( 337 | e.code === 'ResourceNotFound.FunctionName' || 338 | e.code === 'ResourceNotFound.Function' 339 | ) { 340 | return null; 341 | } 342 | 343 | // 除以上错误码的其他信息 344 | throw new CommonError({ 345 | ...ERRORS.OTHER_GET_FAAS_ERROR, 346 | ...{ message: e.message, reqId: e.reqId }, 347 | }); 348 | } 349 | } 350 | 351 | async invokeHTTPFaas({ 352 | name, 353 | namespace = 'default', 354 | event = {}, 355 | }: InvokeOptions): Promise { 356 | const triggers = await this.getTriggers({ 357 | name, 358 | namespace, 359 | }); 360 | if (triggers.length === 0) { 361 | throw new CommonError(ERRORS.WEB_FAAS_NO_TRIGGERS); 362 | } 363 | const { TriggerDesc } = triggers[0]; 364 | const { service } = JSON.parse(TriggerDesc as string); 365 | const baseUrl = service.subDomain; 366 | const { method = 'GET', path = '/', data } = event; 367 | const urlObj = new URL(`${baseUrl}/${path}`); 368 | const realPath = urlObj.pathname.replace(/\/+/g, '/'); 369 | const url = `${urlObj.origin}${realPath}`; 370 | const { body } = await got({ 371 | url, 372 | method, 373 | form: data, 374 | }); 375 | return body; 376 | } 377 | 378 | /** 379 | * 调用函数 380 | * @param {InvokeOptions} options 参数 381 | * @returns {Promise} 函数执行结果 382 | */ 383 | async invoke({ 384 | name, 385 | namespace = 'default', 386 | qualifier = '$LATEST', 387 | event = {}, 388 | logType = LogType.tail, 389 | invokeType = InvokeType.request, 390 | }: InvokeOptions): Promise { 391 | // invoke 之前检查函数是否存在 392 | const detail = await this.get({ 393 | name, 394 | namespace, 395 | qualifier, 396 | }); 397 | if (!detail) { 398 | throw new CommonError(ERRORS.GET_FAAS_ERROR); 399 | } 400 | if (detail.Type === 'HTTP') { 401 | return this.invokeHTTPFaas({ 402 | name, 403 | namespace, 404 | event, 405 | qualifier, 406 | }); 407 | } 408 | const { Result } = await this.request({ 409 | Action: 'Invoke', 410 | FunctionName: name, 411 | Namespace: namespace, 412 | Qualifier: qualifier, 413 | ClientContext: JSON.stringify(event), 414 | LogType: logType, 415 | InvocationType: invokeType, 416 | }); 417 | 418 | return camelCaseProps(Result); 419 | } 420 | 421 | /** 422 | * 获取 faas 的 CLS 配置 423 | * @param {FaasBaseConfig} options 参数 424 | * @returns {Promise} 函数 CLS 配置 425 | */ 426 | async getClsConfig({ 427 | name, 428 | namespace = 'default', 429 | qualifier = '$LATEST', 430 | }: FaasBaseConfig): Promise { 431 | const cacheKey = `${name}-${namespace}-${qualifier}`; 432 | if (this.clsConfigCache[cacheKey]) { 433 | return this.clsConfigCache[cacheKey]; 434 | } 435 | const detail = await this.get({ 436 | name, 437 | namespace, 438 | qualifier, 439 | }); 440 | 441 | if (!detail) { 442 | throw new CommonError(ERRORS.GET_FAAS_ERROR); 443 | } 444 | 445 | const clsConfig = { 446 | logsetId: detail!.ClsLogsetId, 447 | topicId: detail!.ClsTopicId, 448 | }; 449 | this.clsConfigCache[cacheKey] = clsConfig; 450 | return clsConfig; 451 | } 452 | 453 | /** 454 | * 获取函数日志列表,默认 近1个小时 455 | * 注意如果同时定义了 startTime 和 endTime,interval 参数将不起作用 456 | * @param {GetLogOptions} options 参数 457 | * @returns {Promise} 日志列表 458 | */ 459 | async getLogList({ 460 | name, 461 | namespace, 462 | qualifier, 463 | status, 464 | endTime = Date.now(), 465 | interval = 600, 466 | limit = 10, 467 | startTime, 468 | isFilterCompleted = false, 469 | }: GetLogOptions): Promise { 470 | const { logsetId, topicId } = await this.getClsConfig({ 471 | name, 472 | namespace, 473 | qualifier, 474 | }); 475 | 476 | if (!logsetId || !topicId) { 477 | throw new CommonError(ERRORS.GET_CLS_CONFIG_ERROR); 478 | } 479 | 480 | let startDate: Dayjs; 481 | let endDate: Dayjs; 482 | 483 | // 默认获取从当前到一个小时前时间段的日志 484 | if (!endTime) { 485 | endDate = dtz(); 486 | } else { 487 | endDate = dtz(endTime); 488 | } 489 | if (!startTime) { 490 | startDate = dtz(endDate.valueOf() - Number(interval) * 1000); 491 | } else { 492 | startDate = dtz(startTime); 493 | } 494 | 495 | const sql = getSearchSql({ 496 | name, 497 | namespace, 498 | qualifier, 499 | status, 500 | startTime: startDate.valueOf(), 501 | endTime: endDate.valueOf(), 502 | }); 503 | 504 | const searchParameters = { 505 | logset_id: logsetId, 506 | topic_ids: topicId, 507 | start_time: startDate.format(TIME_FORMAT), 508 | end_time: endDate.format(TIME_FORMAT), 509 | query_string: sql, 510 | limit: limit || 10, 511 | sort: 'desc', 512 | }; 513 | const { results = [] } = await this.cls.searchLog(searchParameters); 514 | const logs = []; 515 | for (let i = 0, len = results.length; i < len; i++) { 516 | const curReq = results[i]; 517 | curReq.isCompleted = false; 518 | curReq.startTime = formatDate(curReq.startTime); 519 | 520 | const detailLog = await this.getLogDetail({ 521 | logsetId: logsetId, 522 | topicId: topicId, 523 | reqId: curReq.requestId, 524 | startTime: startDate.format(TIME_FORMAT), 525 | endTime: endDate.format(TIME_FORMAT), 526 | }); 527 | const formatedInfo = formatFaasLog(detailLog || []); 528 | curReq.message = formatedInfo.message; 529 | curReq.memoryUsage = formatedInfo.memoryUsage; 530 | curReq.isCompleted = formatedInfo.isCompleted; 531 | curReq.duration = formatedInfo.duration; 532 | 533 | logs.push(curReq); 534 | } 535 | return isFilterCompleted ? logs.filter((item) => item.isCompleted) : logs; 536 | } 537 | 538 | /** 539 | * 获取请求 ID日志详情,包含多条日志(流式日志),需要自定义拼接 540 | * @param {GetLogDetailOptions} options 参数 541 | * @returns {Promise} 日志详情列表 542 | */ 543 | async getLogDetail({ 544 | startTime, 545 | logsetId, 546 | topicId, 547 | reqId, 548 | endTime = formatDate(Date.now()), 549 | }: GetLogDetailOptions): Promise { 550 | startTime = 551 | startTime || 552 | dtz(endTime) 553 | .add(-1, 'hour') 554 | .format(TIME_FORMAT); 555 | 556 | const sql = `SCF_RequestId:${reqId} AND SCF_RetryNum:0`; 557 | const searchParameters = { 558 | logset_id: logsetId, 559 | topic_ids: topicId, 560 | start_time: startTime as string, 561 | end_time: endTime, 562 | query_string: sql, 563 | limit: 100, 564 | sort: 'asc', 565 | }; 566 | const { results = [] } = await this.cls.searchLog(searchParameters); 567 | 568 | return results; 569 | } 570 | 571 | /** 572 | * 通过请求 ID 获取日志详情 573 | * @param {GetLogOptions} options 参数 574 | * @returns {Promise} 单条日志详情 575 | */ 576 | async getLogByReqId({ 577 | name, 578 | namespace, 579 | qualifier, 580 | endTime = Date.now(), 581 | reqId, 582 | }: GetLogOptions): Promise { 583 | const { logsetId, topicId } = await this.getClsConfig({ 584 | name, 585 | namespace, 586 | qualifier, 587 | }); 588 | 589 | if (!logsetId || !topicId) { 590 | throw new CommonError(ERRORS.GET_CLS_CONFIG_ERROR); 591 | } 592 | 593 | if (!reqId) { 594 | throw new CommonError(ERRORS.REQUEST_ID_INVALID); 595 | } 596 | const endDate = dayjs(endTime); 597 | 598 | if (this.debug) { 599 | console.log(`[FAAS] 通过请求 ID 获取日志: ${reqId}`); 600 | } 601 | 602 | const detailLog = await this.getLogDetail({ 603 | logsetId: logsetId, 604 | topicId: topicId, 605 | reqId, 606 | endTime: formatDate(endDate), 607 | }); 608 | 609 | const curReq: SearchLogItem = { 610 | requestId: reqId, 611 | retryNum: 0, 612 | startTime: '', 613 | memoryUsage: '', 614 | duration: '', 615 | message: '', 616 | isCompleted: false, 617 | }; 618 | const formatedInfo = formatFaasLog(detailLog || []); 619 | curReq.message = formatedInfo.message; 620 | curReq.memoryUsage = formatedInfo.memoryUsage; 621 | curReq.isCompleted = formatedInfo.isCompleted; 622 | curReq.duration = formatedInfo.duration; 623 | 624 | return curReq; 625 | } 626 | 627 | /** 628 | * 获取监控数据,默认获取 Invocation 指标 629 | * @param {GetMonitorDataOptions} options 参数 630 | * @returns 631 | */ 632 | async getMetric({ 633 | metric = 'Invocation', 634 | ...rest 635 | }: GetMonitorDataOptions): Promise { 636 | return this.monitor.get({ 637 | metric, 638 | ...rest, 639 | }); 640 | } 641 | } 642 | -------------------------------------------------------------------------------- /packages/faas/src/monitor/apis.ts: -------------------------------------------------------------------------------- 1 | import { ServiceType, ApiFactory } from '@tencent-sdk/capi'; 2 | 3 | const ACTIONS = ['GetMonitorData'] as const; 4 | 5 | export type ActionType = typeof ACTIONS[number]; 6 | 7 | const APIS = ApiFactory({ 8 | isV3: true, 9 | serviceType: ServiceType.monitor, 10 | version: '2018-07-24', 11 | actions: ACTIONS, 12 | }); 13 | 14 | export default APIS; 15 | -------------------------------------------------------------------------------- /packages/faas/src/monitor/index.ts: -------------------------------------------------------------------------------- 1 | import { Capi, ServiceType } from '@tencent-sdk/capi'; 2 | import { pascalCaseProps } from '@tencent-sdk/common'; 3 | import APIS, { ActionType } from './apis'; 4 | import { dtz, formatDate } from '../dayjs'; 5 | import { 6 | Credentials, 7 | MonitorOptions, 8 | GetMonitorDataOptions, 9 | MonitorData, 10 | FormatedMonitorData, 11 | } from '../typings'; 12 | 13 | /** 14 | * 格式化监控数据 15 | * @param {MonitorData} data raw monitor data 16 | * @returns {FormatedMonitorData[]} 格式化后的监控数据 17 | */ 18 | function formatMetricData(data: MonitorData): FormatedMonitorData[] { 19 | const { DataPoints = [] } = data; 20 | const list: FormatedMonitorData[] = []; 21 | DataPoints.forEach((point) => { 22 | const { Timestamps = [], Values = [] } = point; 23 | Timestamps.forEach((time, i) => { 24 | list.push({ 25 | time: formatDate(time), 26 | value: Values[i], 27 | timestamp: time, 28 | }); 29 | }); 30 | }); 31 | 32 | return list; 33 | } 34 | 35 | export class Monitor { 36 | credentials: Credentials; 37 | capi: Capi; 38 | region: string; 39 | 40 | constructor({ 41 | secretId, 42 | secretKey, 43 | token, 44 | region = 'ap-guangzhou', 45 | debug = false, 46 | }: MonitorOptions) { 47 | this.credentials = { 48 | secretId, 49 | secretKey, 50 | token, 51 | }; 52 | this.region = region; 53 | this.capi = new Capi({ 54 | debug, 55 | region: region, 56 | serviceType: ServiceType.monitor, 57 | secretId: secretId, 58 | secretKey: secretKey, 59 | token: token, 60 | }); 61 | } 62 | 63 | /** 64 | * 获取监控数据 65 | * @param {GetMonitorDataOptions} options 参数 66 | * @returns 67 | */ 68 | async get( 69 | options: GetMonitorDataOptions, 70 | ): Promise { 71 | const { 72 | metric, 73 | name, 74 | namespace = 'default', 75 | alias, 76 | period = 60, 77 | interval = 900, 78 | startTime, 79 | endTime = Date.now(), 80 | isRaw = false, 81 | } = options; 82 | 83 | const endDate = dtz(endTime); 84 | const startDate = startTime 85 | ? dtz(startTime) 86 | : endDate.add(0 - interval, 'second'); 87 | const formatedStartTime = formatDate(startDate, true); 88 | const formatedEndTime = formatDate(endDate, true); 89 | 90 | const dimensions = [ 91 | { 92 | Name: 'namespace', 93 | Value: namespace, 94 | }, 95 | { 96 | Name: 'functionName', 97 | Value: name, 98 | }, 99 | ]; 100 | 101 | if (alias) { 102 | dimensions.push({ 103 | Name: 'alias', 104 | Value: alias, 105 | }); 106 | } 107 | 108 | const res = await this.request({ 109 | MetricName: metric, 110 | Action: 'GetMonitorData', 111 | Namespace: 'QCE/SCF_V2', 112 | Instances: [ 113 | { 114 | Dimensions: dimensions, 115 | }, 116 | ], 117 | Period: period, 118 | StartTime: formatedStartTime, 119 | EndTime: formatedEndTime, 120 | }); 121 | 122 | return isRaw ? res : formatMetricData(res); 123 | } 124 | 125 | async request({ 126 | Action, 127 | ...data 128 | }: { 129 | Action: ActionType; 130 | [key: string]: any; 131 | }) { 132 | const result = await APIS[Action](this.capi, pascalCaseProps(data)); 133 | return result; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /packages/faas/src/typings/index.ts: -------------------------------------------------------------------------------- 1 | export interface AnyObject { 2 | [prop: string]: any; 3 | } 4 | 5 | export type GetFaasOptions = { 6 | // 是否需要获取函数代码,默认设置为 false,提高查询效率 7 | showCode?: boolean; 8 | // 是否需要获取函数触发器,默认设置为 false,提高查询效率 9 | showTriggers?: boolean; 10 | } & FaasBaseConfig; 11 | 12 | export interface FaasBaseConfig { 13 | // 函数名称 14 | name: string; 15 | // 命名空间 16 | namespace?: string; 17 | // 版本 18 | qualifier?: string; 19 | } 20 | 21 | export interface GetVersionsOptions extends FaasBaseConfig { 22 | limit?: number; 23 | page?: number; 24 | order?: string; 25 | orderBy?: string; 26 | } 27 | 28 | export interface GetAliasesOptions extends FaasBaseConfig { 29 | limit?: number; 30 | page?: number; 31 | order?: string; 32 | orderBy?: string; 33 | } 34 | 35 | export interface Credentials { 36 | // secret id 37 | secretId: string; 38 | // secret key 39 | secretKey: string; 40 | // 临时 token 41 | token?: string; 42 | } 43 | 44 | export type FaasOptions = { 45 | region?: string; 46 | debug?: boolean; 47 | } & Credentials; 48 | 49 | export type MonitorOptions = { 50 | region?: string; 51 | debug?: boolean; 52 | } & Credentials; 53 | 54 | export interface Tag { 55 | // 键 56 | Key: string; 57 | // 值 58 | Value: string; 59 | } 60 | 61 | export interface FunctionInfo { 62 | // 函数类型 63 | Type: string; 64 | // 函数名称 65 | FunctionName: string; 66 | // 命名空间 67 | Namespace: string; 68 | // 超时时间 69 | Timeout: number; 70 | // 内存 71 | MemorySize: number; 72 | // 执行方法 73 | Handler: string; 74 | // 运行环境 75 | Runtime: string; 76 | // 状态 77 | Status: string; 78 | // 最新版本 79 | LastVersion: string; 80 | // 异常原因 81 | StatusReasons: { ErrorMessage: string }[]; 82 | // 流量 83 | Traffic?: number; 84 | // 配置流量版本 85 | ConfigTrafficVersion?: string; 86 | // 标签 87 | Tags: Tag[]; 88 | // 日志集ID 89 | ClsLogsetId: string; 90 | // 日志主题ID 91 | ClsTopicId: string; 92 | } 93 | 94 | export interface StatusSqlMap { 95 | // 成功 96 | success: string; 97 | // 失败 98 | fail: string; 99 | // 重试 100 | retry: string; 101 | // 调用中断 102 | interrupt: string; 103 | // 超时超时 104 | timeout: string; 105 | // 调用超时 106 | exceed: string; 107 | // 代码异常 108 | codeError: string; 109 | } 110 | 111 | export interface GetLogOptions { 112 | // 函数名称 113 | name: string; 114 | // 命名空间 115 | namespace?: string; 116 | // 函数版本 117 | qualifier?: string; 118 | // 开始时间,支持格式化的时间和时间戳 119 | startTime?: number | string; 120 | // 结束时间,支持格式化的时间和时间戳 121 | endTime?: number | string; 122 | // 请求 ID 123 | reqId?: string; 124 | // 日志状态 125 | status?: keyof StatusSqlMap | ''; 126 | // 时间间隔,单位秒,默认为 600s 127 | interval?: number; 128 | // 获取条数 129 | limit?: number; 130 | // 是否过滤掉不完整的日志,默认不过滤,设置为 true 可以过滤,只返回完整的日志 131 | isFilterCompleted?: boolean; 132 | } 133 | 134 | export interface ClsConfig { 135 | // 日志集 ID 136 | logsetId: string; 137 | // 日志主题 ID 138 | topicId: string; 139 | } 140 | 141 | export interface LogContent { 142 | // 函数名称 143 | SCF_FunctionName: string; 144 | // 命名空间 145 | SCF_Namespace: string; 146 | // 开始时间 147 | SCF_StartTime: string; 148 | // 请求 ID 149 | SCF_RequestId: string; 150 | // 运行时间 151 | SCF_Duration: string; 152 | // 别名 153 | SCF_Alias: string; 154 | // 版本 155 | SCF_Qualifier: string; 156 | // 日志时间 157 | SCF_LogTime: string; 158 | // 重试次数 159 | SCF_RetryNum: string; 160 | // 使用内存 161 | SCF_MemUsage: string; 162 | // 日志等级 163 | SCF_Level: string; 164 | // 日志信息 165 | SCF_Message: string; 166 | // 日志类型 167 | SCF_Type: string; 168 | // 状态吗 169 | SCF_StatusCode: string; 170 | } 171 | 172 | export type GetLogDetailOptions = { 173 | logsetId: string; 174 | topicId: string; 175 | reqId: string; 176 | // 开始时间 177 | startTime?: string; 178 | // 结束时间 179 | endTime?: string; 180 | }; 181 | 182 | // 通过组装单条 request ID 的详情中 content 的 SCF_Messsage 的日志 183 | export interface SearchLogItem { 184 | requestId: string; 185 | retryNum: number; 186 | startTime: string; 187 | memoryUsage: string; 188 | duration: string; 189 | message: string; 190 | isCompleted: boolean; 191 | } 192 | 193 | // 查询得到的日志详情日志 194 | export interface SearchLogDetailItem { 195 | content: string; 196 | filename: string; 197 | pkg_id: string; 198 | pkg_logid: string; 199 | source: string; 200 | time: number; 201 | timestamp: string; 202 | topic_id: string; 203 | topic_name: string; 204 | } 205 | 206 | export enum LogType { 207 | none = 'None', 208 | tail = 'Tail', 209 | } 210 | 211 | export enum InvokeType { 212 | request = 'RequestResponse', 213 | event = 'Event', 214 | } 215 | 216 | export interface InvokeOptions { 217 | // 函数名称 218 | name: string; 219 | // 命名空间 220 | namespace?: string; 221 | // 版本号 222 | qualifier?: string; 223 | // 触发器事件 224 | event?: Record; 225 | // 日志类型 226 | logType?: LogType; 227 | // 执行类型 228 | invokeType?: InvokeType; 229 | } 230 | 231 | export interface InvokeResult { 232 | // 计费时间 233 | billDuration: number; 234 | // 运行时间 235 | duration: number; 236 | // 错误信息 237 | errMsg: string; 238 | // 运行内存 239 | memUsage: number; 240 | // 请求 ID 241 | functionRequestId: string; 242 | // 没用字段 243 | invokeResult: number; 244 | // 函数运行日志 245 | log: string; 246 | // 函数返回结果 247 | retMsg: string; 248 | } 249 | export interface GetMonitorDataOptions { 250 | // 指标名称,参考云函数监控指标文档:https://cloud.tencent.com/document/product/248/45130 251 | metric: string; 252 | // 函数名称 253 | name: string; 254 | // 命名空间 255 | namespace?: string; 256 | // 别名,默认流量,$LATEST 257 | alias?: string; 258 | // 时间间隔,单位秒,默认为 900s 259 | interval?: number; 260 | // 统计周期,单位秒,默认为 60s 261 | period?: number; 262 | // 开始时间, 格式:2018-09-22T19:51:23+08:00 263 | startTime?: string; 264 | // 结束时间, 格式:2018-09-22T19:51:23+08:00 265 | endTime?: string; 266 | 267 | // 是否需要获取接口源数据,默认为 false,返回格式化后的数据 268 | isRaw?: boolean; 269 | } 270 | 271 | export interface DataPoint { 272 | Timestamps: number[]; 273 | Values: any[]; 274 | Dimensions: any[]; 275 | } 276 | export interface MonitorData { 277 | StartTime: string; 278 | EndTime: string; 279 | Period: number; 280 | MetricName: string; 281 | DataPoints: DataPoint[]; 282 | RequestId: string; 283 | } 284 | 285 | export interface FormatedMonitorData { 286 | time: string; 287 | value: any; 288 | timestamp: number; 289 | } 290 | 291 | export interface GetTriggersOptions { 292 | name: string; 293 | namespace?: string; 294 | page?: number; 295 | } 296 | 297 | export interface TriggerData { 298 | Type: string; 299 | TriggerDesc?: string; 300 | TriggerName?: string; 301 | Qualifier?: string; 302 | } 303 | 304 | export interface Alias { 305 | Name: string; 306 | Description: string; 307 | FunctionVersion: string; 308 | RoutingConfig: { 309 | AdditionalVersionWeights: any[]; 310 | AddtionVersionMatchs: any[]; 311 | }; 312 | AddTime: string; 313 | ModTime: string; 314 | } 315 | 316 | export interface Version { 317 | Version: string; 318 | Description: string; 319 | AddTime: string; 320 | ModTime: string; 321 | Status: string; 322 | } 323 | -------------------------------------------------------------------------------- /packages/faas/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { LogContent } from './typings/index'; 2 | import { StatusSqlMap, GetLogOptions } from './typings'; 3 | 4 | const StatusMap: StatusSqlMap = { 5 | success: 'SCF_StatusCode=200', 6 | fail: 7 | 'SCF_StatusCode != 200 AND SCF_StatusCode != 202 AND SCF_StatusCode != 499', 8 | retry: 'SCF_RetryNum > 0', 9 | interrupt: 'SCF_StatusCode = 499', 10 | timeout: 'SCF_StatusCode = 433', 11 | exceed: 'SCF_StatusCode = 434', 12 | codeError: 'SCF_StatusCode = 500', 13 | }; 14 | 15 | export function formatWhere({ 16 | name, 17 | namespace = 'default', 18 | qualifier = '$LATEST', 19 | status, 20 | startTime, 21 | endTime, 22 | }: Partial) { 23 | let where = `SCF_Namespace='${namespace}' AND SCF_Qualifier='${qualifier}'`; 24 | if (startTime && endTime) { 25 | where += ` AND (SCF_StartTime between ${startTime} AND ${endTime})`; 26 | } 27 | if (name) { 28 | where += ` AND SCF_FunctionName='${name}'`; 29 | } 30 | if (status) { 31 | where += ` AND ${StatusMap[status]}'`; 32 | } 33 | 34 | return where; 35 | } 36 | 37 | export function getSearchSql(options: GetLogOptions) { 38 | const where = formatWhere(options); 39 | const sql = `* | SELECT SCF_RequestId as requestId, SCF_RetryNum as retryNum, MAX(SCF_StartTime) as startTime WHERE ${where} GROUP BY SCF_RequestId, SCF_RetryNum ORDER BY startTime desc`; 40 | 41 | return sql; 42 | } 43 | 44 | /** 45 | * 判断是否包含字符串 46 | * @param {string} msg - string 47 | * @returns {boolean} 48 | */ 49 | export function isContain(msg: string, subStr: string) { 50 | return msg.indexOf(subStr) > -1; 51 | } 52 | 53 | function hasStartMark(msg: string) { 54 | return isContain(msg, 'START RequestId'); 55 | } 56 | function hasEndMark(msg: string) { 57 | return isContain(msg, 'END RequestId'); 58 | } 59 | function hasReportMark(msg: string) { 60 | return isContain(msg, 'Report RequestId'); 61 | } 62 | 63 | /** 64 | * 判断是否是完整的日志 65 | * @param {string} msg - log message 66 | * @returns {boolean} 67 | */ 68 | export function isCompleted(msg: string) { 69 | return hasStartMark(msg) && hasEndMark(msg) && hasReportMark(msg); 70 | } 71 | 72 | export function formatFaasLog(detailLog: { content: string }[]) { 73 | let memoryUsage = ''; 74 | let duration = ''; 75 | 76 | const message = (detailLog || []) 77 | .map(({ content }: { content: string }) => { 78 | try { 79 | const info = JSON.parse(content) as LogContent; 80 | if (info.SCF_Type === 'Custom') { 81 | memoryUsage = info.SCF_MemUsage; 82 | duration = info.SCF_Duration; 83 | } 84 | return info.SCF_Message; 85 | } catch (e) { 86 | return ''; 87 | } 88 | }) 89 | .join(''); 90 | return { 91 | memoryUsage, 92 | duration, 93 | message, 94 | isCompleted: isCompleted(message), 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /packages/faas/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "../../typings"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/login/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.map 3 | tsconfig.tsbuildinfo 4 | test 5 | tests 6 | __test__ 7 | __tests__ 8 | src 9 | tsconfig.json 10 | -------------------------------------------------------------------------------- /packages/login/README.md: -------------------------------------------------------------------------------- 1 | # Tencent Cloud Wechat Login 2 | 3 | Tencent Cloud Wechat Login Tool. 4 | 5 | ## Usage 6 | 7 | Before use, you need create an instance: 8 | 9 | ```js 10 | import { TencentLogin } from '@tencent-sdk/login'; 11 | const tencentLogin = new TencentLogin(); 12 | 13 | // login 14 | const loginData = await tLogin.login(); 15 | console.log('Login Result: ', loginData); 16 | 17 | // refresh auth info 18 | const res = await tLogin.refresh( 19 | loginData.uuid, 20 | loginData.expired, 21 | loginData.signature, 22 | loginData.appid, 23 | ); 24 | 25 | console.log('Flush Result: ', res); 26 | ``` 27 | 28 | ## License 29 | 30 | MIT 31 | -------------------------------------------------------------------------------- /packages/login/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { TencentLogin } from '../src'; 3 | 4 | describe('Login', () => { 5 | const client = new TencentLogin(); 6 | test('should get login url', async () => { 7 | const uuid = uuidv4(); 8 | const apiUrl = await client.getShortUrl(uuid); 9 | 10 | expect(apiUrl).toEqual({ 11 | login_status_url: expect.any(String), 12 | long_url: expect.stringContaining( 13 | 'https://cloud.tencent.com/open/authorize', 14 | ), 15 | short_url: expect.stringContaining('https://slslogin.qcloud.com'), 16 | success: true, 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/login/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tencent-sdk/login", 3 | "version": "0.1.5", 4 | "description": "Tencent cloud serverlerss login sdk", 5 | "main": "dist/index.js", 6 | "node": "dist/index.js", 7 | "browser": "dist/index.js", 8 | "module": "dist/index.esm.js", 9 | "jsnext:main": "dist/index.esm.js", 10 | "types": "dist/index.d.ts", 11 | "typings": "dist/index.d.ts", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "scripts": { 16 | "test": "ts-node ./test/index.spec.ts", 17 | "clean": "rimraf ./dist tsconfig.tsbuildinfo" 18 | }, 19 | "keywords": [ 20 | "tencent-cloud", 21 | "login" 22 | ], 23 | "author": "yugasun", 24 | "license": "MIT", 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/yugasun/tencent-sdk.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/yugasun/tencent-sdk/issues" 31 | }, 32 | "homepage": "https://github.com/yugasun/tencent-sdk#readme", 33 | "dependencies": { 34 | "@types/qrcode": "^1.3.4", 35 | "@types/uuid": "^3.4.6", 36 | "@ygkit/request": "^0.0.6", 37 | "axios": "^0.20.0", 38 | "qrcode": "^1.4.4", 39 | "uuid": "^3.3.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/login/src/constant.ts: -------------------------------------------------------------------------------- 1 | export const API_BASE_URL = 'http://scfdev.tencentserverless.com'; 2 | export const API_SHORT_URL = `${API_BASE_URL}/login/url`; 3 | export const REFRESH_TOKEN_URL = `${API_BASE_URL}/login/info`; 4 | export const ONE_SECOND = 1 * 1000; 5 | -------------------------------------------------------------------------------- /packages/login/src/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { type } from 'os'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | import * as qrcode from 'qrcode'; 5 | import { sleep, waitResponse } from '@ygkit/request'; 6 | import { API_BASE_URL, API_SHORT_URL, REFRESH_TOKEN_URL } from './constant'; 7 | 8 | export interface ApiUrl { 9 | login_status_url: string; 10 | long_url: string; 11 | short_url: string; 12 | success: boolean; 13 | } 14 | 15 | export interface LoginData { 16 | secret_id: string; 17 | secret_key: string; 18 | token: string; 19 | appid: string; 20 | signature: string; 21 | expired: number; 22 | } 23 | 24 | export interface LoginResult { 25 | uuid: string; 26 | secret_id: string; 27 | secret_key: string; 28 | token: string; 29 | appid: string; 30 | signature: string; 31 | expired: number; 32 | } 33 | 34 | export class TencentLogin { 35 | // timeout in seconds 36 | public TIMEOUT: number = 60000; 37 | 38 | /** 39 | * convert string to qrcode 40 | * @param str string need to be print to qrcode 41 | */ 42 | async printQrCode(str: string) { 43 | const url = await qrcode.toString(str, { 44 | type: 'utf8', 45 | errorCorrectionLevel: 'M', 46 | }); 47 | // print cyan color qrcode 48 | console.log(`\u001b[36m ${url} \u001b[39m`); 49 | } 50 | 51 | /** 52 | * GET request 53 | * @param url request url 54 | */ 55 | async getRequest(url: string): Promise { 56 | try { 57 | const { data } = await axios.get(url); 58 | 59 | if (data.success !== true) { 60 | return false; 61 | } 62 | return data; 63 | } catch (e) { 64 | return false; 65 | } 66 | } 67 | 68 | /** 69 | * get short url 70 | * @param uuid uuid 71 | */ 72 | async getShortUrl(uuid: string): Promise { 73 | const url = `${API_SHORT_URL}?os=${type()}&uuid=${uuid}`; 74 | return this.getRequest(url); 75 | } 76 | 77 | /** 78 | * check auth status 79 | * @param uuid uuid 80 | * @param url auth url 81 | */ 82 | async checkStatus(url: string): Promise { 83 | const tokenUrl = `${API_BASE_URL}${url}`; 84 | try { 85 | const { data } = await axios.get(tokenUrl); 86 | 87 | if (data.success !== true) { 88 | return false; 89 | } 90 | 91 | return data; 92 | } catch (e) { 93 | return false; 94 | } 95 | } 96 | 97 | /** 98 | * refresh auth token 99 | * @param uuid uuid 100 | * @param expired expire time 101 | * @param signature signature 102 | * @param appid app id 103 | */ 104 | async refresh( 105 | uuid: string, 106 | expired: number, 107 | signature: string, 108 | appid: number | string, 109 | ): Promise { 110 | const url = `${REFRESH_TOKEN_URL}?uuid=${uuid}&os=${type()}&expired=${expired}&signature=${signature}&appid=${appid}`; 111 | return this.getRequest(url); 112 | } 113 | 114 | async login(): Promise { 115 | try { 116 | const uuid = uuidv4(); 117 | const apiUrl = await this.getShortUrl(uuid); 118 | // 1. print qrcode 119 | await this.printQrCode(apiUrl.short_url); 120 | 121 | console.log('Please scan QR code login from wechat'); 122 | console.log('Wait login...'); 123 | // wait 3s start check login status 124 | await sleep(3000); 125 | try { 126 | // 2. loop get login status 127 | const loginData = await waitResponse({ 128 | callback: async () => this.checkStatus(apiUrl.login_status_url), 129 | timeout: this.TIMEOUT, 130 | targetProp: 'success', 131 | targetResponse: true, 132 | }); 133 | 134 | const configure = { 135 | secret_id: loginData.secret_id, 136 | secret_key: loginData.secret_key, 137 | token: loginData.token, 138 | appid: loginData.appid, 139 | signature: loginData.signature, 140 | expired: loginData.expired, 141 | uuid: uuid, 142 | } as LoginResult; 143 | console.log('Login successful for TencentCloud'); 144 | return configure; 145 | } catch (e) { 146 | console.log('Login timeout. please login again'); 147 | process.exit(0); 148 | } 149 | } catch (e) { 150 | console.log(e.message); 151 | } 152 | process.exit(0); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /packages/login/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "../../typings/**/*.d.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /scripts/bundle.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | import * as rollup from 'rollup'; 5 | import json from 'rollup-plugin-json'; 6 | import globals from 'rollup-plugin-node-globals'; 7 | import resolve from 'rollup-plugin-node-resolve'; 8 | import typescript2 from 'rollup-plugin-typescript2'; 9 | import commonjs from 'rollup-plugin-commonjs'; 10 | 11 | import ts from 'typescript'; 12 | 13 | import project from './project'; 14 | import { c, createLogger } from './logger'; 15 | 16 | const logPreProcess = createLogger('preprocess'); 17 | const logBundle = createLogger('bundle'); 18 | 19 | function getKeys(p) { 20 | const packageJsonFile = `${process.cwd()}/packages/${p}/package.json`; 21 | const data = fs.readFileSync(packageJsonFile, 'utf-8'); 22 | const { dependencies } = JSON.parse(data); 23 | return dependencies ? Object.keys(dependencies) : []; 24 | } 25 | 26 | async function bundle() { 27 | try { 28 | const argv = process.argv.slice(2); 29 | const outputs = argv[0].split(','); 30 | const pkgName = argv[1]; 31 | console.log('pkgName', pkgName); 32 | 33 | const packages = project.packages; 34 | 35 | const count = packages.length; 36 | let cur = 0; 37 | 38 | async function bundlePkg(pkg) { 39 | const logPrefix = c.grey(`[${++cur}/${count}] ${pkg.scopedName}`); 40 | logBundle(`${logPrefix} creating bundle`); 41 | 42 | const externals = packages 43 | .filter((p) => p.name !== pkg.name) 44 | .map((p) => p.scopedName); 45 | 46 | logBundle(`externals: ${externals}`); 47 | 48 | const bundle = await rollup.rollup({ 49 | input: path.join(pkg.src, 'index.ts'), 50 | plugins: [ 51 | resolve({ 52 | browser: true, 53 | jsnext: true, 54 | preferBuiltins: true, 55 | }), 56 | commonjs(), 57 | globals(), 58 | json(), 59 | typescript2({ 60 | tsconfig: path.join(pkg.path, 'tsconfig.json'), 61 | typescript: ts, // ensure we're using the same typescript (3.x) for rollup as for regular builds etc 62 | tsconfigOverride: { 63 | module: 'esnext', 64 | stripInternal: true, 65 | emitDeclarationOnly: false, 66 | composite: false, 67 | declaration: false, 68 | declarationMap: false, 69 | sourceMap: true, 70 | esModuleInterop: true, 71 | }, 72 | }), 73 | ], 74 | external: getKeys(pkg.name), 75 | }); 76 | 77 | // 'amd' | 'cjs' | 'system' | 'es' | 'esm' | 'iife' | 'umd' 78 | if (outputs.indexOf('esm') === -1) { 79 | logBundle(`${logPrefix} skipping esm`); 80 | } else { 81 | logBundle(`${logPrefix} writing esm - ${pkg.esm}`); 82 | 83 | await bundle.write({ 84 | file: pkg.esm, 85 | name: pkg.globalName, 86 | format: 'esm', 87 | sourcemap: true, 88 | }); 89 | } 90 | 91 | if (outputs.indexOf('umd') === -1) { 92 | logBundle(`${logPrefix} skipping umd`); 93 | } else { 94 | logBundle(`${logPrefix} writing umd - ${pkg.umd}`); 95 | 96 | await bundle.write({ 97 | file: pkg.umd, 98 | exports: 'named', 99 | name: pkg.globalName, 100 | globals: { 101 | ...getKeys(pkg.name).reduce((g, packages) => { 102 | if (packages === pkg.name) { 103 | g[pkg.scopedName] = pkg.globalName; 104 | } else { 105 | g[packages] = packages; 106 | } 107 | return g; 108 | }, {}), 109 | tslib: 'tslib', 110 | }, 111 | format: 'umd', 112 | sourcemap: true, 113 | }); 114 | } 115 | } 116 | 117 | for (const pkg of packages) { 118 | if (pkgName) { 119 | if (pkg.name === pkgName) { 120 | await bundlePkg(pkg); 121 | break; 122 | } 123 | } else { 124 | await bundlePkg(pkg); 125 | } 126 | } 127 | } catch (err) { 128 | logBundle('Failed to bundle:'); 129 | logBundle(err); 130 | process.exit(1); 131 | } 132 | } 133 | 134 | bundle(); 135 | -------------------------------------------------------------------------------- /scripts/logger.ts: -------------------------------------------------------------------------------- 1 | import * as l from 'fancy-log'; 2 | const log = ((l).default || l); 3 | import * as c from 'chalk'; 4 | const chalk = (c.default || c); 5 | 6 | export function createLogger(name: string): typeof log { 7 | const prefix = `> ${chalk.green(name)} `; 8 | const logger = log.bind(log, prefix); 9 | logger.info = log.info.bind(log, prefix); 10 | logger.dir = log.dir.bind(log, prefix); 11 | logger.warn = log.warn.bind(log, prefix); 12 | logger.error = log.error.bind(log, prefix); 13 | return logger; 14 | } 15 | 16 | export { chalk as c }; 17 | -------------------------------------------------------------------------------- /scripts/project.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import camelcase from 'camelcase'; 4 | import * as lernaJson from '../lerna.json'; 5 | 6 | const rootPath = path.resolve(__dirname, '..'); 7 | const includesPath = path.join(rootPath, 'includes'); 8 | const packagesPath = path.join(rootPath, 'packages'); 9 | 10 | export default { 11 | lerna: lernaJson, 12 | packages: fs 13 | .readdirSync(packagesPath) 14 | .filter((p) => fs.lstatSync(path.join(packagesPath, p)).isDirectory()) 15 | .map((p) => { 16 | const pkgName = path.basename(p); 17 | const pkgGlobalName = camelcase(pkgName); 18 | const pkgPath = path.join(packagesPath, p); 19 | const pkgSrc = path.join(pkgPath, 'src'); 20 | const pkgScopedName = p; 21 | const pkgDist = path.join(pkgPath, 'dist'); 22 | 23 | const pkgUmd = path.join(pkgDist, 'index.js'); 24 | const pkgEsm = path.join(pkgDist, 'index.esm.js'); 25 | 26 | return { 27 | name: pkgName, 28 | globalName: pkgGlobalName, 29 | scopedName: pkgScopedName, 30 | path: pkgPath, 31 | src: pkgSrc, 32 | dist: pkgDist, 33 | umd: pkgUmd, 34 | esm: pkgEsm, 35 | }; 36 | }), 37 | }; 38 | -------------------------------------------------------------------------------- /scripts/publish.ts: -------------------------------------------------------------------------------- 1 | import lerna from 'lerna'; 2 | import * as childproc from 'child_process'; 3 | 4 | import project from './project'; 5 | import { createLogger, c } from './logger'; 6 | 7 | const DIST_TAG = 'next'; 8 | 9 | const log = createLogger('publish'); 10 | 11 | const getVersion = async () => { 12 | const re = /(\d+)\.(\d+)\.(\d+)($|\-)/; 13 | const match = project.lerna.version.match(re); 14 | 15 | if (match === null) { 16 | throw new Error('Lerna version is malformed.'); 17 | } 18 | 19 | const [, major, minor, patch] = match; 20 | 21 | return { major, minor, patch }; 22 | }; 23 | 24 | const getCommitSHA = async () => { 25 | return new Promise((resolve, reject) => { 26 | childproc.exec('git rev-parse --short=7 HEAD', (err, stdout) => { 27 | if (err) { 28 | return reject(err); 29 | } 30 | 31 | resolve(stdout); 32 | }); 33 | }); 34 | }; 35 | 36 | const getDate = (sep?: string): string => { 37 | const s = sep === undefined ? '' : sep; 38 | const raw = new Date() 39 | .toISOString() 40 | .replace(/:|T|\.|-/g, '') 41 | .slice(0, 8); 42 | const y = raw.slice(0, 4); 43 | const m = raw.slice(4, 6); 44 | const d = raw.slice(6, 8); 45 | return `${y}${s}${m}${s}${d}`; 46 | }; 47 | 48 | const publish = async () => { 49 | if (process.env.CI && process.env.TRAVIS_BRANCH !== 'master') { 50 | return; 51 | } 52 | 53 | try { 54 | const { major, minor, patch } = await getVersion(); 55 | const sha = await getCommitSHA(); 56 | const version = `${major}.${minor}.${patch}-${DIST_TAG}.${getDate()}`; 57 | 58 | lerna([ 59 | 'publish', 60 | version, 61 | '--npm-tag', 62 | DIST_TAG, 63 | '--exact', 64 | '--no-git-tag-version', 65 | '--no-push', 66 | '--no-verify-access', 67 | '--no-verify-registry', 68 | '-y', 69 | ]); 70 | 71 | return version; 72 | } catch (err) { 73 | log(`Could not publish: ${err}`); 74 | throw err; 75 | } 76 | }; 77 | 78 | publish().then((v) => log(`Published new packages with version ${v}`)); 79 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "module": "commonjs", 5 | "target": "es2015", 6 | "resolveJsonModule": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": ".", 5 | "composite": true, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "downlevelIteration": true, 9 | "emitDecoratorMetadata": true, 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "preserveConstEnums": true, 13 | "importHelpers": true, 14 | "lib": ["esnext", "dom"], 15 | "module": "commonjs", 16 | "moduleResolution": "node", 17 | "sourceMap": true, 18 | "paths": { 19 | "*": ["typings/*", "includes/*"] 20 | }, 21 | "resolveJsonModule": true, 22 | "noUnusedLocals": true, 23 | "strict": true, 24 | "target": "es5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "packages/capi" }, 5 | { "path": "packages/login" }, 6 | { "path": "packages/cls" }, 7 | { "path": "packages/faas" }, 8 | { "path": "packages/capi-web" } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-config-prettier"], 3 | "rules": { 4 | "interface-name": [true, "never-prefix"], 5 | "member-access": false, 6 | "no-angle-bracket-type-assertion": false, 7 | "no-bitwise": false, 8 | "no-console": false, 9 | "no-default-export": true, 10 | "no-empty-interface": false, 11 | "no-implicit-dependencies": false, 12 | "no-submodule-imports": false, 13 | "ordered-imports": [false], 14 | "object-literal-sort-keys": false, 15 | "object-literal-key-quotes": [true, "as-needed"], 16 | "quotemark": [true, "single"], 17 | "semicolon": [true, "always", "ignore-bound-class-methods"], 18 | "jsx-boolean-value": false 19 | }, 20 | "linterOptions": { 21 | "exclude": ["config/**/*.js", "node_modules/**/*.ts"] 22 | } 23 | } 24 | --------------------------------------------------------------------------------