From bd120d11266f3478f0350f47991ea95b6a4c172b Mon Sep 17 00:00:00 2001 From: cool Date: Wed, 20 Sep 2023 10:12:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 4 + .gitignore | 20 + README.md | 4 + cloud/.editorconfig | 11 + cloud/.eslintrc.json | 20 + cloud/.gitignore | 15 + cloud/.prettierrc.js | 3 + cloud/LICENSE | 21 + cloud/index.d.ts | 10 + cloud/jest.config.js | 7 + cloud/jest.setup.js | 1 + cloud/package.json | 46 ++ cloud/src/LICENSE | 21 + cloud/src/config/config.default.ts | 4 + cloud/src/configuration.ts | 22 + cloud/src/db/index.ts | 131 ++++ cloud/src/db/source.ts | 10 + cloud/src/func/crud.ts | 525 +++++++++++++++ cloud/src/func/index.ts | 17 + cloud/src/index.ts | 10 + cloud/src/interface.ts | 11 + cloud/src/package.json | 41 ++ cloud/src/util.ts | 1 + cloud/tsconfig.json | 25 + core/LICENSE | 21 + core/README.md | 3 + core/_.editorconfig | 11 + core/_.eslintrc.json | 7 + core/_.gitignore | 15 + core/_.prettierrc.js | 3 + core/index.d.ts | 10 + core/jest.config.js | 7 + core/jest.setup.js | 1 + core/package.json | 55 ++ core/src/LICENSE | 21 + core/src/config/config.default.ts | 20 + core/src/configuration.ts | 82 +++ core/src/constant/global.ts | 84 +++ core/src/controller/base.ts | 205 ++++++ core/src/decorator/cache.ts | 8 + core/src/decorator/controller.ts | 208 ++++++ core/src/decorator/event.ts | 42 ++ core/src/decorator/index.ts | 109 ++++ core/src/decorator/tag.ts | 50 ++ core/src/decorator/transaction.ts | 19 + core/src/entity/base.ts | 27 + core/src/entity/mongo.ts | 25 + core/src/entity/typeorm.ts | 3 + core/src/event/index.ts | 43 ++ core/src/exception/base.ts | 13 + core/src/exception/comm.ts | 16 + core/src/exception/core.ts | 16 + core/src/exception/filter.ts | 21 + core/src/exception/validate.ts | 16 + core/src/index.ts | 46 ++ core/src/interface.ts | 390 +++++++++++ core/src/module/config.ts | 100 +++ core/src/module/import.ts | 155 +++++ core/src/module/menu.ts | 76 +++ core/src/package.json | 54 ++ core/src/rest/eps.ts | 124 ++++ core/src/service/base.ts | 526 +++++++++++++++ core/src/tag/data.ts | 63 ++ core/src/util/func.ts | 27 + core/src/util/location.ts | 95 +++ core/tsconfig.json | 25 + es/.editorconfig | 11 + es/.eslintrc.json | 28 + es/.gitignore | 15 + es/.prettierrc.js | 3 + es/README.md | 3 + es/index.d.ts | 10 + es/jest.config.js | 7 + es/jest.setup.js | 1 + es/package.json | 43 ++ es/src/base.ts | 615 ++++++++++++++++++ es/src/config/config.default.ts | 4 + es/src/configuration.ts | 19 + es/src/decorator/elasticsearch.ts | 41 ++ es/src/elasticsearch.ts | 154 +++++ es/src/index.ts | 18 + es/src/package.json | 41 ++ es/tsconfig.json | 24 + file/.editorconfig | 11 + file/.eslintrc.json | 20 + file/.gitignore | 15 + file/.prettierrc.js | 3 + file/LICENSE | 21 + file/index.d.ts | 10 + file/jest.config.js | 7 + file/jest.setup.js | 1 + file/package.json | 52 ++ file/src/config/config.default.ts | 16 + file/src/configuration.ts | 21 + file/src/file.ts | 476 ++++++++++++++ file/src/index.ts | 5 + file/src/interface.ts | 105 +++ file/src/package.json | 52 ++ file/tsconfig.json | 25 + iot/.editorconfig | 11 + iot/.eslintrc.json | 28 + iot/.gitignore | 15 + iot/.prettierrc.js | 3 + iot/README.md | 3 + iot/index.d.ts | 10 + iot/jest.config.js | 7 + iot/jest.setup.js | 1 + iot/package.json | 52 ++ iot/src/config/config.default.ts | 13 + iot/src/configuration.ts | 18 + iot/src/decorator/mqtt.ts | 42 ++ iot/src/index.ts | 7 + iot/src/interface.ts | 34 + iot/src/mqtt.ts | 163 +++++ iot/src/package.json | 52 ++ iot/tsconfig.json | 24 + other/cache-manager-fs-hash/LICENSE | 21 + other/cache-manager-fs-hash/README.md | 81 +++ other/cache-manager-fs-hash/index.js | 1 + other/cache-manager-fs-hash/package.json | 37 ++ other/cache-manager-fs-hash/src/index.js | 261 ++++++++ .../src/json-file-store.js | 118 ++++ .../src/wrap-callback.js | 21 + other/mqemitter-redis/.github/dependabot.yml | 7 + .../mqemitter-redis/.github/workflows/ci.yml | 35 + other/mqemitter-redis/.gitignore | 33 + other/mqemitter-redis/.travis.yml | 9 + other/mqemitter-redis/LICENSE | 21 + other/mqemitter-redis/README.md | 76 +++ other/mqemitter-redis/mqemitter-redis.js | 250 +++++++ other/mqemitter-redis/package.json | 44 ++ other/mqemitter-redis/types/index.d.ts | 37 ++ pay/.editorconfig | 11 + pay/.eslintrc.json | 28 + pay/.gitignore | 15 + pay/.prettierrc.js | 3 + pay/index.d.ts | 10 + pay/jest.config.js | 7 + pay/jest.setup.js | 1 + pay/package.json | 49 ++ pay/src/ali.ts | 56 ++ pay/src/config/config.default.ts | 4 + pay/src/configuration.ts | 21 + pay/src/index.ts | 7 + pay/src/interface.ts | 77 +++ pay/src/package.json | 49 ++ pay/src/wx.ts | 68 ++ pay/tsconfig.json | 24 + rpc/.editorconfig | 11 + rpc/.eslintrc.json | 29 + rpc/.gitignore | 15 + rpc/.prettierrc.js | 3 + rpc/index.d.ts | 10 + rpc/jest.config.js | 7 + rpc/jest.setup.js | 1 + rpc/package.json | 47 ++ rpc/src/config/config.default.ts | 6 + rpc/src/configuration.ts | 26 + rpc/src/decorator/event/event.ts | 19 + rpc/src/decorator/event/handler.ts | 23 + rpc/src/decorator/index.ts | 101 +++ rpc/src/decorator/rpc.ts | 84 +++ rpc/src/decorator/transaction.ts | 22 + rpc/src/index.ts | 25 + rpc/src/package.json | 47 ++ rpc/src/rpc.ts | 275 ++++++++ rpc/src/service/base.ts | 406 ++++++++++++ rpc/src/test.ts | 25 + rpc/src/transaction/event.ts | 40 ++ rpc/tsconfig.json | 24 + sms/.editorconfig | 11 + sms/.eslintrc.json | 28 + sms/.gitignore | 15 + sms/.prettierrc.js | 3 + sms/index.d.ts | 10 + sms/jest.config.js | 7 + sms/jest.setup.js | 1 + sms/package.json | 48 ++ sms/src/ali.ts | 64 ++ sms/src/config/config.default.ts | 5 + sms/src/configuration.ts | 17 + sms/src/index.ts | 9 + sms/src/interface.ts | 80 +++ sms/src/package.json | 48 ++ sms/src/sms.ts | 83 +++ sms/src/tx.ts | 70 ++ sms/src/yp.ts | 86 +++ sms/tsconfig.json | 24 + task/.editorconfig | 11 + task/.eslintrc.json | 28 + task/.gitignore | 15 + task/.prettierrc.js | 3 + task/README.md | 3 + task/index.d.ts | 10 + task/jest.config.js | 7 + task/jest.setup.js | 1 + task/package.json | 50 ++ task/src/base.ts | 118 ++++ task/src/config/config.default.ts | 6 + task/src/configuration.ts | 19 + task/src/decorator/queue.ts | 26 + task/src/index.ts | 7 + task/src/package.json | 50 ++ task/src/queue.ts | 142 ++++ task/tsconfig.json | 24 + tsconfig.json | 25 + 206 files changed, 10064 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 cloud/.editorconfig create mode 100644 cloud/.eslintrc.json create mode 100644 cloud/.gitignore create mode 100644 cloud/.prettierrc.js create mode 100644 cloud/LICENSE create mode 100644 cloud/index.d.ts create mode 100644 cloud/jest.config.js create mode 100644 cloud/jest.setup.js create mode 100644 cloud/package.json create mode 100644 cloud/src/LICENSE create mode 100644 cloud/src/config/config.default.ts create mode 100644 cloud/src/configuration.ts create mode 100644 cloud/src/db/index.ts create mode 100644 cloud/src/db/source.ts create mode 100644 cloud/src/func/crud.ts create mode 100644 cloud/src/func/index.ts create mode 100644 cloud/src/index.ts create mode 100644 cloud/src/interface.ts create mode 100644 cloud/src/package.json create mode 100644 cloud/src/util.ts create mode 100644 cloud/tsconfig.json create mode 100644 core/LICENSE create mode 100644 core/README.md create mode 100644 core/_.editorconfig create mode 100644 core/_.eslintrc.json create mode 100644 core/_.gitignore create mode 100644 core/_.prettierrc.js create mode 100644 core/index.d.ts create mode 100644 core/jest.config.js create mode 100644 core/jest.setup.js create mode 100644 core/package.json create mode 100644 core/src/LICENSE create mode 100644 core/src/config/config.default.ts create mode 100644 core/src/configuration.ts create mode 100644 core/src/constant/global.ts create mode 100644 core/src/controller/base.ts create mode 100644 core/src/decorator/cache.ts create mode 100644 core/src/decorator/controller.ts create mode 100644 core/src/decorator/event.ts create mode 100644 core/src/decorator/index.ts create mode 100644 core/src/decorator/tag.ts create mode 100644 core/src/decorator/transaction.ts create mode 100644 core/src/entity/base.ts create mode 100644 core/src/entity/mongo.ts create mode 100644 core/src/entity/typeorm.ts create mode 100644 core/src/event/index.ts create mode 100644 core/src/exception/base.ts create mode 100644 core/src/exception/comm.ts create mode 100644 core/src/exception/core.ts create mode 100644 core/src/exception/filter.ts create mode 100644 core/src/exception/validate.ts create mode 100644 core/src/index.ts create mode 100644 core/src/interface.ts create mode 100644 core/src/module/config.ts create mode 100644 core/src/module/import.ts create mode 100644 core/src/module/menu.ts create mode 100644 core/src/package.json create mode 100644 core/src/rest/eps.ts create mode 100644 core/src/service/base.ts create mode 100644 core/src/tag/data.ts create mode 100644 core/src/util/func.ts create mode 100644 core/src/util/location.ts create mode 100644 core/tsconfig.json create mode 100644 es/.editorconfig create mode 100644 es/.eslintrc.json create mode 100644 es/.gitignore create mode 100644 es/.prettierrc.js create mode 100644 es/README.md create mode 100644 es/index.d.ts create mode 100644 es/jest.config.js create mode 100644 es/jest.setup.js create mode 100644 es/package.json create mode 100644 es/src/base.ts create mode 100644 es/src/config/config.default.ts create mode 100644 es/src/configuration.ts create mode 100644 es/src/decorator/elasticsearch.ts create mode 100644 es/src/elasticsearch.ts create mode 100644 es/src/index.ts create mode 100644 es/src/package.json create mode 100644 es/tsconfig.json create mode 100644 file/.editorconfig create mode 100644 file/.eslintrc.json create mode 100644 file/.gitignore create mode 100644 file/.prettierrc.js create mode 100644 file/LICENSE create mode 100644 file/index.d.ts create mode 100644 file/jest.config.js create mode 100644 file/jest.setup.js create mode 100644 file/package.json create mode 100644 file/src/config/config.default.ts create mode 100644 file/src/configuration.ts create mode 100644 file/src/file.ts create mode 100644 file/src/index.ts create mode 100644 file/src/interface.ts create mode 100644 file/src/package.json create mode 100644 file/tsconfig.json create mode 100644 iot/.editorconfig create mode 100644 iot/.eslintrc.json create mode 100644 iot/.gitignore create mode 100644 iot/.prettierrc.js create mode 100644 iot/README.md create mode 100644 iot/index.d.ts create mode 100644 iot/jest.config.js create mode 100644 iot/jest.setup.js create mode 100644 iot/package.json create mode 100644 iot/src/config/config.default.ts create mode 100644 iot/src/configuration.ts create mode 100644 iot/src/decorator/mqtt.ts create mode 100644 iot/src/index.ts create mode 100644 iot/src/interface.ts create mode 100644 iot/src/mqtt.ts create mode 100644 iot/src/package.json create mode 100644 iot/tsconfig.json create mode 100644 other/cache-manager-fs-hash/LICENSE create mode 100644 other/cache-manager-fs-hash/README.md create mode 100644 other/cache-manager-fs-hash/index.js create mode 100644 other/cache-manager-fs-hash/package.json create mode 100644 other/cache-manager-fs-hash/src/index.js create mode 100644 other/cache-manager-fs-hash/src/json-file-store.js create mode 100644 other/cache-manager-fs-hash/src/wrap-callback.js create mode 100644 other/mqemitter-redis/.github/dependabot.yml create mode 100644 other/mqemitter-redis/.github/workflows/ci.yml create mode 100644 other/mqemitter-redis/.gitignore create mode 100644 other/mqemitter-redis/.travis.yml create mode 100644 other/mqemitter-redis/LICENSE create mode 100644 other/mqemitter-redis/README.md create mode 100644 other/mqemitter-redis/mqemitter-redis.js create mode 100644 other/mqemitter-redis/package.json create mode 100644 other/mqemitter-redis/types/index.d.ts create mode 100644 pay/.editorconfig create mode 100644 pay/.eslintrc.json create mode 100644 pay/.gitignore create mode 100644 pay/.prettierrc.js create mode 100644 pay/index.d.ts create mode 100644 pay/jest.config.js create mode 100644 pay/jest.setup.js create mode 100644 pay/package.json create mode 100644 pay/src/ali.ts create mode 100644 pay/src/config/config.default.ts create mode 100644 pay/src/configuration.ts create mode 100644 pay/src/index.ts create mode 100644 pay/src/interface.ts create mode 100644 pay/src/package.json create mode 100644 pay/src/wx.ts create mode 100644 pay/tsconfig.json create mode 100644 rpc/.editorconfig create mode 100644 rpc/.eslintrc.json create mode 100644 rpc/.gitignore create mode 100644 rpc/.prettierrc.js create mode 100644 rpc/index.d.ts create mode 100644 rpc/jest.config.js create mode 100644 rpc/jest.setup.js create mode 100644 rpc/package.json create mode 100644 rpc/src/config/config.default.ts create mode 100644 rpc/src/configuration.ts create mode 100644 rpc/src/decorator/event/event.ts create mode 100644 rpc/src/decorator/event/handler.ts create mode 100644 rpc/src/decorator/index.ts create mode 100644 rpc/src/decorator/rpc.ts create mode 100644 rpc/src/decorator/transaction.ts create mode 100644 rpc/src/index.ts create mode 100644 rpc/src/package.json create mode 100644 rpc/src/rpc.ts create mode 100644 rpc/src/service/base.ts create mode 100644 rpc/src/test.ts create mode 100644 rpc/src/transaction/event.ts create mode 100644 rpc/tsconfig.json create mode 100644 sms/.editorconfig create mode 100644 sms/.eslintrc.json create mode 100644 sms/.gitignore create mode 100644 sms/.prettierrc.js create mode 100644 sms/index.d.ts create mode 100644 sms/jest.config.js create mode 100644 sms/jest.setup.js create mode 100644 sms/package.json create mode 100644 sms/src/ali.ts create mode 100644 sms/src/config/config.default.ts create mode 100644 sms/src/configuration.ts create mode 100644 sms/src/index.ts create mode 100644 sms/src/interface.ts create mode 100644 sms/src/package.json create mode 100644 sms/src/sms.ts create mode 100644 sms/src/tx.ts create mode 100644 sms/src/yp.ts create mode 100644 sms/tsconfig.json create mode 100644 task/.editorconfig create mode 100644 task/.eslintrc.json create mode 100644 task/.gitignore create mode 100644 task/.prettierrc.js create mode 100644 task/README.md create mode 100644 task/index.d.ts create mode 100644 task/jest.config.js create mode 100644 task/jest.setup.js create mode 100644 task/package.json create mode 100644 task/src/base.ts create mode 100644 task/src/config/config.default.ts create mode 100644 task/src/configuration.ts create mode 100644 task/src/decorator/queue.ts create mode 100644 task/src/index.ts create mode 100644 task/src/package.json create mode 100644 task/src/queue.ts create mode 100644 task/tsconfig.json create mode 100644 tsconfig.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0b909d8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +*.js text eol=lf +*.json text eol=lf +*.ts text eol=lf +*.code-snippets text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4cafa26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +logs/ +cache/ +npm-debug.log +yarn-error.log +node_modules/ +package-lock.json +yarn.lock +coverage/ +dist/ +.idea/ +run/ +.DS_Store +launch.json +*.sw* +*.un~ +.tsbuildinfo +.tsbuildinfo.* +data/* +pnpm-lock.yaml +public/uploads/* diff --git a/README.md b/README.md index 7ff654a..30bfa49 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # cool-admin-midway-packages cool-admin midway 后端核心包 + +- 为了不和src代码相互影响,7.0的核心依赖包单独成一个项目 + +- 7.0之前的在cool-admin-midway这个项目的packages目录下 \ No newline at end of file diff --git a/cloud/.editorconfig b/cloud/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/cloud/.editorconfig @@ -0,0 +1,11 @@ +# 🎨 editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/cloud/.eslintrc.json b/cloud/.eslintrc.json new file mode 100644 index 0000000..93e32bd --- /dev/null +++ b/cloud/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "extends": "./node_modules/mwts/", + "ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"], + "env": { + "jest": true + }, + "rules": { + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/ban-ts-comment": "off", + "node/no-extraneous-import": "off", + "no-empty": "off", + "node/no-extraneous-require": "off", + "eqeqeq": "off", + "node/no-unsupported-features/node-builtins": "off", + "@typescript-eslint/ban-types": "off", + "no-control-regex": "off", + "prefer-const": "off" + } +} diff --git a/cloud/.gitignore b/cloud/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/cloud/.gitignore @@ -0,0 +1,15 @@ +logs/ +npm-debug.log +yarn-error.log +node_modules/ +package-lock.json +yarn.lock +coverage/ +dist/ +.idea/ +run/ +.DS_Store +*.sw* +*.un~ +.tsbuildinfo +.tsbuildinfo.* diff --git a/cloud/.prettierrc.js b/cloud/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/cloud/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/cloud/LICENSE b/cloud/LICENSE new file mode 100644 index 0000000..5a739ee --- /dev/null +++ b/cloud/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013 - Now midwayjs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/cloud/index.d.ts b/cloud/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/cloud/index.d.ts @@ -0,0 +1,10 @@ +export * from './dist/index'; + +declare module '@midwayjs/core/dist/interface' { + interface MidwayConfig { + book?: PowerPartial<{ + a: number; + b: string; + }>; + } +} diff --git a/cloud/jest.config.js b/cloud/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/cloud/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], + setupFilesAfterEnv: ['./jest.setup.js'] +}; diff --git a/cloud/jest.setup.js b/cloud/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/cloud/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/cloud/package.json b/cloud/package.json new file mode 100644 index 0000000..7aa5cdc --- /dev/null +++ b/cloud/package.json @@ -0,0 +1,46 @@ +{ + "name": "@cool-midway/cloud", + "version": "7.0.0", + "description": "", + "main": "dist/index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [ + "cool", + "cool-admin", + "cooljs" + ], + "author": "COOL", + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts", + "index.d.ts" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^2.0.0", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@midwayjs/typeorm": "^3.9.0", + "@types/jest": "^29.2.4", + "@types/node": "^18.11.15", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "lodash": "^4.17.21", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typeorm": "^0.3.11", + "typescript": "^4.9.4" + } +} diff --git a/cloud/src/LICENSE b/cloud/src/LICENSE new file mode 100644 index 0000000..5a739ee --- /dev/null +++ b/cloud/src/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013 - Now midwayjs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/cloud/src/config/config.default.ts b/cloud/src/config/config.default.ts new file mode 100644 index 0000000..03ef2cd --- /dev/null +++ b/cloud/src/config/config.default.ts @@ -0,0 +1,4 @@ +/** + * cool的配置 + */ +export default {}; diff --git a/cloud/src/configuration.ts b/cloud/src/configuration.ts new file mode 100644 index 0000000..ee7447f --- /dev/null +++ b/cloud/src/configuration.ts @@ -0,0 +1,22 @@ +import { ILifeCycle, ILogger, IMidwayContainer, Logger } from '@midwayjs/core'; +import { Configuration } from '@midwayjs/decorator'; +import * as DefaultConfig from './config/config.default'; +import { CoolCloudDb } from './db'; + +@Configuration({ + namespace: 'cloud', + importConfigs: [ + { + default: DefaultConfig, + }, + ], +}) +export class CoolCloudConfiguration implements ILifeCycle { + @Logger() + coreLogger: ILogger; + + async onReady(container: IMidwayContainer) { + await container.getAsync(CoolCloudDb); + this.coreLogger.info('\x1B[36m [cool:cloud] ready \x1B[0m'); + } +} diff --git a/cloud/src/db/index.ts b/cloud/src/db/index.ts new file mode 100644 index 0000000..fa9fac4 --- /dev/null +++ b/cloud/src/db/index.ts @@ -0,0 +1,131 @@ +import { CoolCommException } from '@cool-midway/core'; +import { CoolDataSource } from './source'; +import { + ALL, + Config, + ILogger, + Init, + Logger, + Provide, + Scope, + ScopeEnum, +} from '@midwayjs/core'; +import { Repository } from 'typeorm'; +import * as ts from 'typescript'; +import * as _ from 'lodash'; +/** + * 数据库 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolCloudDb { + @Logger() + coreLogger: ILogger; + + coolDataSource: CoolDataSource; + + @Config(ALL) + config; + + @Init() + async init() { + const config = this.config.typeorm.dataSource.default; + if (!config) { + throw new CoolCommException('未配置数据库default信息'); + } + this.coolDataSource = new CoolDataSource({ + ...this.config.typeorm.dataSource.default, + entities: [], + }); + // 连接数据库 + await this.coolDataSource.initialize(); + } + + /** + * 获得数据库操作实例 + * @param tableClass 表类 + * @param appId 应用ID + * @returns + */ + getRepository(tableClass: string, appId = 'CLOUD'): Repository { + return this.coolDataSource.getRepository(`${tableClass}${appId}`); + } + + /** + * 创建表 + * @param table 表结构,元函数,字符串 + * @param appId 应用ID,确保每个应用的数据隔离 + * @param synchronize 是否同步表结构 + */ + async createTable(table: string, synchronize = false, appId = 'CLOUD') { + if (!table || !appId) { + throw new CoolCommException('table、appId不能为空'); + } + const { newCode, className } = this.parseCode(table, appId); + const entities = this.coolDataSource.options.entities; + // @ts-ignore + this.coolDataSource.options.entities = _.dropWhile(entities, { + name: className, + }); + const code = ts.transpile( + `${newCode} + this.coolDataSource.options.entities.push(${className}) + + this.coolDataSource.buildMetadatas().then(() => { + if(synchronize){ + this.coolDataSource.synchronize(); + } + }); + `, + { + emitDecoratorMetadata: true, + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2018, + removeComments: true, + experimentalDecorators: true, + noImplicitThis: true, + noUnusedLocals: true, + stripInternal: true, + skipLibCheck: true, + pretty: true, + declaration: true, + noImplicitAny: false, + } + ); + eval(code); + } + + /** + * 根据字符串查找并生成一个跟appId相关的类名 + * @param code 代码 + * @param appId + */ + parseCode(code: string, appId = 'CLOUD') { + try { + const oldClassName = code + .match('class(.*)extends')[1] + .replace(/\s*/g, ''); + const oldTableStart = code.indexOf('@Entity('); + const oldTableEnd = code.indexOf(')'); + + const oldTableName = code + .substring(oldTableStart + 9, oldTableEnd - 1) + .replace(/\s*/g, '') + // eslint-disable-next-line no-useless-escape + .replace(/\"/g, '') + // eslint-disable-next-line no-useless-escape + .replace(/\'/g, ''); + const className = `${oldClassName}${appId}`; + return { + newCode: code + .replace(oldClassName, className) + .replace(oldTableName, `func_${oldTableName}`), + className, + tableName: `func_${oldTableName}`, + }; + } catch (err) { + this.coreLogger.error(err); + throw new CoolCommException('代码结构不正确,请检查'); + } + } +} diff --git a/cloud/src/db/source.ts b/cloud/src/db/source.ts new file mode 100644 index 0000000..f5c13af --- /dev/null +++ b/cloud/src/db/source.ts @@ -0,0 +1,10 @@ +import { DataSource } from 'typeorm'; + +export class CoolDataSource extends DataSource { + /** + * 重新构造元数据 + */ + async buildMetadatas() { + await super.buildMetadatas(); + } +} diff --git a/cloud/src/func/crud.ts b/cloud/src/func/crud.ts new file mode 100644 index 0000000..842c4cd --- /dev/null +++ b/cloud/src/func/crud.ts @@ -0,0 +1,525 @@ +import { CloudReq } from './../interface'; +import { IMidwayApplication } from '@midwayjs/core'; +import { + CoolConfig, + CoolEventManager, + CoolValidateException, + CurdOption, + ERRINFO, + EVENT, +} from '@cool-midway/core'; +import { Brackets, In, Repository, SelectQueryBuilder } from 'typeorm'; +import { CoolCloudDb } from '../db'; +import * as _ from 'lodash'; +import * as SqlString from 'sqlstring'; + +export abstract class CloudCrud { + ctx; + + curdOption: CurdOption; + + coolCloudDb: CoolCloudDb; + + coolConfig: CoolConfig; + + entity: Repository; + + app: IMidwayApplication; + + req: CloudReq; + + coolEventManager: CoolEventManager; + + protected sqlParams; + + setCurdOption(curdOption: CurdOption) { + this.curdOption = curdOption; + } + + /** + * 设置实体 + * @param entityModel + */ + async setEntity() { + this.entity = this.coolCloudDb.getRepository( + this.curdOption.entity, + 'CLOUD' + ); + } + + abstract main(req: CloudReq): Promise; + + async init(req: CloudReq) { + this.sqlParams = []; + // 执行主函数 + await this.main(req); + // 操作之前 + await this.before(); + // // 设置实体 + await this.setEntity(); + } + + /** + * 参数安全性检查 + * @param params + */ + async paramSafetyCheck(params) { + const lp = params.toLowerCase(); + return !( + lp.indexOf('update ') > -1 || + lp.indexOf('select ') > -1 || + lp.indexOf('delete ') > -1 || + lp.indexOf('insert ') > -1 + ); + } + + /** + * 非分页查询 + * @param query 查询条件 + * @param option 查询配置 + */ + async list(query): Promise { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + const sql = await this.getOptionFind(query, this.curdOption.listQueryOp); + return this.nativeQuery(sql, []); + } + + /** + * 执行SQL并获得分页数据 + * @param sql 执行的sql语句 + * @param query 分页查询条件 + * @param autoSort 是否自动排序 + */ + async sqlRenderPage(sql, query, autoSort = true) { + const { + size = this.coolConfig.crud.pageSize, + page = 1, + order = 'createTime', + sort = 'desc', + isExport = false, + maxExportLimit, + } = query; + if (order && sort && autoSort) { + if (!(await this.paramSafetyCheck(order + sort))) { + throw new CoolValidateException('非法传参~'); + } + sql += ` ORDER BY ${SqlString.escapeId(order)} ${this.checkSort(sort)}`; + } + if (isExport && maxExportLimit > 0) { + this.sqlParams.push(parseInt(maxExportLimit)); + sql += ' LIMIT ? '; + } + if (!isExport) { + this.sqlParams.push((page - 1) * size); + this.sqlParams.push(parseInt(size)); + sql += ' LIMIT ?,? '; + } + + let params = []; + params = params.concat(this.sqlParams); + const result = await this.nativeQuery(sql, params); + const countResult = await this.nativeQuery(this.getCountSql(sql), params); + return { + list: result, + pagination: { + page: parseInt(page), + size: parseInt(size), + total: parseInt(countResult[0] ? countResult[0].count : 0), + }, + }; + } + + /** + * 分页查询 + * @param connectionName 连接名 + */ + async page(query) { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + const sql = await this.getOptionFind(query, this.curdOption.pageQueryOp); + return this.sqlRenderPage(sql, query, false); + } + + /** + * 获得查询个数的SQL + * @param sql + */ + getCountSql(sql) { + sql = sql + .replace(new RegExp('LIMIT', 'gm'), 'limit ') + .replace(new RegExp('\n', 'gm'), ' '); + if (sql.includes('limit')) { + const sqlArr = sql.split('limit '); + sqlArr.pop(); + sql = sqlArr.join('limit '); + } + return `select count(*) as count from (${sql}) a`; + } + + /** + * 操作entity获得分页数据,不用写sql + * @param find QueryBuilder + * @param query + * @param autoSort + * @param connectionName + */ + async entityRenderPage( + find: SelectQueryBuilder, + query, + autoSort = true + ) { + const { + size = this.coolConfig.crud.pageSize, + page = 1, + order = 'createTime', + sort = 'desc', + isExport = false, + maxExportLimit, + } = query; + const count = await find.getCount(); + let dataFind: SelectQueryBuilder; + if (isExport && maxExportLimit > 0) { + dataFind = find.limit(maxExportLimit); + } else { + dataFind = find.offset((page - 1) * size).limit(size); + } + if (autoSort) { + find.addOrderBy(order, sort.toUpperCase()); + } + return { + list: await dataFind.getRawMany(), + pagination: { + page: parseInt(page), + size: parseInt(size), + total: count, + }, + }; + } + + /** + * 检查排序 + * @param sort 排序 + * @returns + */ + private checkSort(sort) { + if (!['desc', 'asc'].includes(sort.toLowerCase())) { + throw new CoolValidateException('sort 非法传参~'); + } + return sort; + } + + /** + * 原生查询 + * @param sql + * @param params + */ + async nativeQuery(sql, params?) { + if (_.isEmpty(params)) { + params = this.sqlParams; + } + let newParams = []; + newParams = newParams.concat(params); + this.sqlParams = []; + for (const param of newParams) { + SqlString.escape(param); + } + return await this.getOrmManager().query(sql, newParams || []); + } + + /** + * 获得ORM管理 + * @param connectionName 连接名称 + */ + getOrmManager() { + return this.coolCloudDb.coolDataSource; + } + + private async before() { + if (!this.curdOption?.before) { + return; + } + await this.curdOption.before(this.ctx, this.app); + } + + /** + * 插入参数值 + * @param curdOption 配置 + */ + private async insertParam(param) { + if (!this.curdOption?.insertParam) { + return param; + } + return { + ...param, + ...(await this.curdOption.insertParam(this.ctx, this.app)), + }; + } + + /** + * 新增|修改|删除 之后的操作 + * @param data 对应数据 + */ + async modifyAfter( + data: any, + type: 'delete' | 'update' | 'add' + ): Promise {} + + /** + * 新增|修改|删除 之前的操作 + * @param data 对应数据 + */ + async modifyBefore( + data: any, + type: 'delete' | 'update' | 'add' + ): Promise {} + + /** + * 新增 + * @param param + * @returns + */ + async add(param) { + param = await this.insertParam(param); + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + await this.modifyBefore(param, 'add'); + await this.addOrUpdate(param); + await this.modifyAfter(param, 'add'); + return { + id: + param instanceof Array + ? param.map(e => { + return e.id ? e.id : e._id; + }) + : param.id + ? param.id + : param._id, + }; + } + + /** + * 新增|修改 + * @param param 数据 + */ + async addOrUpdate(param: any | any[]) { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + delete param.createTime; + if (param.id) { + param.updateTime = new Date(); + await this.entity.update(param.id, param); + } else { + param.createTime = new Date(); + param.updateTime = new Date(); + await this.entity.insert(param); + } + } + + /** + * 删除 + * @param ids 删除的ID集合 如:[1,2,3] 或者 1,2,3 + */ + async delete(ids: any) { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + await this.modifyBefore(ids, 'delete'); + if (ids instanceof String) { + ids = ids.split(','); + } + if (this.coolConfig.crud?.softDelete) { + this.softDelete(ids); + } + await this.entity.delete(ids); + await this.modifyAfter(ids, 'delete'); + } + + /** + * 软删除 + * @param ids 删除的ID数组 + * @param entity 实体 + */ + async softDelete(ids: string[], entity?: Repository, userId?: string) { + const data = await this.entity.find({ + where: { + id: In(ids), + }, + }); + if (_.isEmpty(data)) return; + const _entity = entity ? entity : this.entity; + const params = { + data, + ctx: this.ctx, + entity: _entity, + }; + if (data.length > 0) { + this.coolEventManager.emit(EVENT.SOFT_DELETE, params); + } + } + + /** + * 修改 + * @param param 数据 + */ + async update(param: any) { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + await this.modifyBefore(param, 'update'); + if (!param.id && !(param instanceof Array)) + throw new CoolValidateException(ERRINFO.NOID); + await this.addOrUpdate(param); + await this.modifyAfter(param, 'update'); + } + + /** + * 获得单个ID + * @param id ID + */ + async info(id: any): Promise { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + if (!id) { + throw new CoolValidateException(ERRINFO.NOID); + } + const info = await this.entity.findBy({ id }); + if (info && this.curdOption?.infoIgnoreProperty) { + for (const property of this.curdOption?.infoIgnoreProperty) { + delete info[property]; + } + } + return info; + } + + /** + * 构建查询配置 + * @param query 前端查询 + * @param option + */ + private async getOptionFind(query, option) { + let { order = 'createTime', sort = 'desc', keyWord = '' } = query; + const sqlArr = ['SELECT']; + const selects = ['a.*']; + const find = this.entity.createQueryBuilder('a'); + if (option) { + if (typeof option == 'function') { + // @ts-ignore + option = await option(this.baseCtx, this.baseApp); + } + // 判断是否有关联查询,有的话取个别名 + if (!_.isEmpty(option.join)) { + for (const item of option.join) { + selects.push(`${item.alias}.*`); + find[item.type || 'leftJoin']( + item.entity, + item.alias, + item.condition + ); + } + } + // 默认条件 + if (option.where) { + const wheres = + typeof option.where == 'function' + ? await option.where(this.ctx, this.app) + : option.where; + if (!_.isEmpty(wheres)) { + for (const item of wheres) { + if ( + item.length == 2 || + (item.length == 3 && + (item[2] || (item[2] === 0 && item[2] != ''))) + ) { + for (const key in item[1]) { + this.sqlParams.push(item[1][key]); + } + find.andWhere(item[0], item[1]); + } + } + } + } + // 附加排序 + if (!_.isEmpty(option.addOrderBy)) { + for (const key in option.addOrderBy) { + if (order && order == key) { + sort = option.addOrderBy[key].toUpperCase(); + } + find.addOrderBy( + SqlString.escapeId(key), + this.checkSort(option.addOrderBy[key].toUpperCase()) + ); + } + } + // 关键字模糊搜索 + if (keyWord || (keyWord == 0 && keyWord != '')) { + keyWord = `%${keyWord}%`; + find.andWhere( + new Brackets(qb => { + const keyWordLikeFields = option.keyWordLikeFields; + for (let i = 0; i < option.keyWordLikeFields?.length || 0; i++) { + qb.orWhere(`${keyWordLikeFields[i]} like :keyWord`, { + keyWord, + }); + this.sqlParams.push(keyWord); + } + }) + ); + } + // 筛选字段 + if (!_.isEmpty(option.select)) { + sqlArr.push(option.select.join(',')); + find.select(option.select); + } else { + sqlArr.push(selects.join(',')); + } + // 字段全匹配 + if (!_.isEmpty(option.fieldEq)) { + for (const key of option.fieldEq) { + const c = {}; + // 单表字段无别名的情况下操作 + if (typeof key === 'string') { + if (query[key] || (query[key] == 0 && query[key] == '')) { + c[key] = query[key]; + const eq = query[key] instanceof Array ? 'in' : '='; + if (eq === 'in') { + find.andWhere(`${key} ${eq} (:${key})`, c); + } else { + find.andWhere(`${key} ${eq} :${key}`, c); + } + this.sqlParams.push(query[key]); + } + } else { + if ( + query[key.requestParam] || + (query[key.requestParam] == 0 && query[key.requestParam] !== '') + ) { + c[key.column] = query[key.requestParam]; + const eq = query[key.requestParam] instanceof Array ? 'in' : '='; + if (eq === 'in') { + find.andWhere(`${key.column} ${eq} (:${key.column})`, c); + } else { + find.andWhere(`${key.column} ${eq} :${key.column}`, c); + } + this.sqlParams.push(query[key.requestParam]); + } + } + } + } + } else { + sqlArr.push(selects.join(',')); + } + // 接口请求的排序 + if (sort && order) { + const sorts = sort.toUpperCase().split(','); + const orders = order.split(','); + if (sorts.length != orders.length) { + throw new CoolValidateException(ERRINFO.SORTFIELD); + } + for (const i in sorts) { + find.addOrderBy( + SqlString.escapeId(orders[i]), + this.checkSort(sorts[i]) + ); + } + } + if (option?.extend) { + await option?.extend(find, this.ctx, this.app); + } + const sqls = find.getSql().split('FROM'); + sqlArr.push('FROM'); + sqlArr.push(sqls[1]); + return sqlArr.join(' '); + } +} diff --git a/cloud/src/func/index.ts b/cloud/src/func/index.ts new file mode 100644 index 0000000..94da053 --- /dev/null +++ b/cloud/src/func/index.ts @@ -0,0 +1,17 @@ +import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; + +/** + * 云函数 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolCloudFunc { + /** + * 获得类名 + * @param code + * @returns + */ + getClassName(code: string) { + return code.match('class(.*)extends')[1].replace(/\s*/g, ''); + } +} diff --git a/cloud/src/index.ts b/cloud/src/index.ts new file mode 100644 index 0000000..b95e149 --- /dev/null +++ b/cloud/src/index.ts @@ -0,0 +1,10 @@ +export { CoolCloudConfiguration as Configuration } from './configuration'; + +export * from './interface'; + +// 云数据库 +export * from './db/index'; + +// 云函数 +export * from './func/index'; +export * from './func/crud'; diff --git a/cloud/src/interface.ts b/cloud/src/interface.ts new file mode 100644 index 0000000..3c36af0 --- /dev/null +++ b/cloud/src/interface.ts @@ -0,0 +1,11 @@ +/** + * 云函数请求 + */ +export interface CloudReq { + // 云函数名称 + name: string; + // 请求参数 + params: any; + // 调用方法 + method: string; +} diff --git a/cloud/src/package.json b/cloud/src/package.json new file mode 100644 index 0000000..370e479 --- /dev/null +++ b/cloud/src/package.json @@ -0,0 +1,41 @@ +{ + "name": "@cool-midway/cloud", + "version": "7.0.0", + "description": "", + "main": "index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": ["cool","cool-admin","cooljs"], + "author": "COOL", + "files": [ + "**/*.js", + "**/*.d.ts", + "index.d.ts" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^2.0.0", + "@midwayjs/core": "^3.0.0", + "@midwayjs/decorator": "^3.0.0", + "@midwayjs/mock": "^3.0.0", + "@midwayjs/typeorm": "^3.8.3", + "@types/jest": "^29.2.0", + "@types/node": "^16.11.22", + "cross-env": "^6.0.0", + "jest": "^29.2.2", + "mwts": "^1.0.5", + "ts-jest": "^29.0.3", + "typeorm": "^0.3.11", + "typescript": "^4.9.4" + } +} diff --git a/cloud/src/util.ts b/cloud/src/util.ts new file mode 100644 index 0000000..f98361e --- /dev/null +++ b/cloud/src/util.ts @@ -0,0 +1 @@ +export class CoolCloudUtil {} diff --git a/cloud/tsconfig.json b/cloud/tsconfig.json new file mode 100644 index 0000000..9bb4ab7 --- /dev/null +++ b/cloud/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "inlineSourceMap":false, + "noImplicitThis": true, + "noUnusedLocals": true, + "stripInternal": true, + "skipLibCheck": true, + "noImplicitReturns": false, + "pretty": true, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/core/LICENSE b/core/LICENSE new file mode 100644 index 0000000..5a739ee --- /dev/null +++ b/core/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013 - Now midwayjs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000..610c3db --- /dev/null +++ b/core/README.md @@ -0,0 +1,3 @@ +# cool-admin + +https://cool-js.com diff --git a/core/_.editorconfig b/core/_.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/core/_.editorconfig @@ -0,0 +1,11 @@ +# 🎨 editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/core/_.eslintrc.json b/core/_.eslintrc.json new file mode 100644 index 0000000..8d20e22 --- /dev/null +++ b/core/_.eslintrc.json @@ -0,0 +1,7 @@ +{ + "extends": "./node_modules/mwts/", + "ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"], + "env": { + "jest": true + } +} diff --git a/core/_.gitignore b/core/_.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/core/_.gitignore @@ -0,0 +1,15 @@ +logs/ +npm-debug.log +yarn-error.log +node_modules/ +package-lock.json +yarn.lock +coverage/ +dist/ +.idea/ +run/ +.DS_Store +*.sw* +*.un~ +.tsbuildinfo +.tsbuildinfo.* diff --git a/core/_.prettierrc.js b/core/_.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/core/_.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/core/index.d.ts b/core/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/core/index.d.ts @@ -0,0 +1,10 @@ +export * from './dist/index'; + +declare module '@midwayjs/core/dist/interface' { + interface MidwayConfig { + book?: PowerPartial<{ + a: number; + b: string; + }>; + } +} diff --git a/core/jest.config.js b/core/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/core/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], + setupFilesAfterEnv: ['./jest.setup.js'] +}; diff --git a/core/jest.setup.js b/core/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/core/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/core/package.json b/core/package.json new file mode 100644 index 0000000..25e1181 --- /dev/null +++ b/core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@cool-midway/core", + "version": "7.0.0-beta1", + "description": "", + "main": "dist/index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [ + "cool", + "cool-admin", + "cooljs" + ], + "author": "COOL", + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts", + "index.d.ts" + ], + "readme": "README.md", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "devDependencies": { + "@midwayjs/cli": "1.3.21", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/koa": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@midwayjs/typeorm": "^3.9.0", + "@types/jest": "^29.2.4", + "@types/node": "^18.11.15", + "aedes": "^0.48.1", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typeorm": "^0.3.11", + "typescript": "~4.9.4" + }, + "dependencies": { + "@midwayjs/cache": "^3.9.0", + "lodash": "^4.17.21", + "md5": "^2.3.0", + "moment": "^2.29.4", + "mysql2-import": "^5.0.22", + "sqlstring": "^2.3.3" + } +} diff --git a/core/src/LICENSE b/core/src/LICENSE new file mode 100644 index 0000000..5a739ee --- /dev/null +++ b/core/src/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013 - Now midwayjs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/core/src/config/config.default.ts b/core/src/config/config.default.ts new file mode 100644 index 0000000..8f23754 --- /dev/null +++ b/core/src/config/config.default.ts @@ -0,0 +1,20 @@ +import { CoolConfig } from "../interface"; + +/** + * cool的配置 + */ +export default { + cool: { + // 是否自动导入数据库 + initDB: false, + // 是否自动导入模块菜单 + initMenu: true, + // crud配置 + crud: { + // 软删除 + softDelete: true, + // 分页查询每页条数 + pageSize: 15, + }, + } as CoolConfig, +}; diff --git a/core/src/configuration.ts b/core/src/configuration.ts new file mode 100644 index 0000000..4204797 --- /dev/null +++ b/core/src/configuration.ts @@ -0,0 +1,82 @@ +import { + App, + Context, + ILifeCycle, + ILogger, + IMidwayBaseApplication, + IMidwayContainer, + Inject, + Logger, +} from "@midwayjs/core"; +import { Configuration } from "@midwayjs/decorator"; +import * as DefaultConfig from "./config/config.default"; +import { CoolExceptionFilter } from "./exception/filter"; +import { FuncUtil } from "./util/func"; +import location from "./util/location"; +import * as koa from "@midwayjs/koa"; +import { CoolModuleConfig } from "./module/config"; +import { CoolModuleImport } from "./module/import"; +import { CoolEventManager } from "./event"; +import { CoolEps } from "./rest/eps"; +import { CacheManager } from "@midwayjs/cache"; +import * as cache from "@midwayjs/cache"; +import { CoolDecorator } from "./decorator"; + +@Configuration({ + namespace: "cool", + imports: [cache], + importConfigs: [ + { + default: DefaultConfig, + }, + ], +}) +export class CoolConfiguration implements ILifeCycle { + @Logger() + coreLogger: ILogger; + + @App() + app: koa.Application; + + @Inject() + coolEventManager: CoolEventManager; + + async onReady(container: IMidwayContainer) { + this.coolEventManager.emit("onReady"); + // 处理模块配置 + await container.getAsync(CoolModuleConfig); + // 导入模块数据 + await container.getAsync(CoolModuleImport); + // 常用函数处理 + await container.getAsync(FuncUtil); + // 事件 + await container.getAsync(CoolEventManager); + // 异常处理 + this.app.useFilter([CoolExceptionFilter]); + // 装饰器 + await container.getAsync(CoolDecorator); + + if (this.app.getEnv() == "local") { + // 实体与路径 + const eps: CoolEps = await container.getAsync(CoolEps); + eps.init(); + } + // 缓存设置为全局 + global["COOL-CACHE"] = await container.getAsync(CacheManager); + // 清除 location + setTimeout(() => { + location.clean(); + this.coreLogger.info("\x1B[36m [cool:core] location clean \x1B[0m"); + }, 10000); + } + + async onConfigLoad( + container: IMidwayContainer, + mainApp?: IMidwayBaseApplication + ) {} + + async onServerReady() { + this.coolEventManager.emit("onServerReady"); + location.clean(); + } +} diff --git a/core/src/constant/global.ts b/core/src/constant/global.ts new file mode 100644 index 0000000..ad18ebe --- /dev/null +++ b/core/src/constant/global.ts @@ -0,0 +1,84 @@ +/** + * 返回码 + */ +export enum RESCODE { + // 成功 + SUCCESS = 1000, + // 失败 + COMMFAIL = 1001, + // 参数验证失败 + VALIDATEFAIL = 1002, + // 参数验证失败 + COREFAIL = 1003, +} + +/** + * 返回信息 + */ +export enum RESMESSAGE { + // 成功 + SUCCESS = "success", + // 失败 + COMMFAIL = "comm fail", + // 参数验证失败 + VALIDATEFAIL = "validate fail", + // 核心异常 + COREFAIL = "core fail", +} + +/** + * 错误提示 + */ +export enum ERRINFO { + NOENTITY = "未设置操作实体", + NOID = "查询参数[id]不存在", + SORTFIELD = "排序参数不正确", +} + +/** + * 事件 + */ +export enum EVENT { + // 软删除 + SOFT_DELETE = "onSoftDelete", + // 服务成功启动 + SERVER_READY = "onServerReady", + // 服务就绪 + READY = "onReady", + // ES 数据改变 + ES_DATA_CHANGE = "esDataChange", +} + + +export class GlobalConfig { + private static instance: GlobalConfig; + + RESCODE = { + SUCCESS: 1000, + COMMFAIL: 1001, + VALIDATEFAIL: 1002, + COREFAIL: 1003, + }; + + RESMESSAGE = { + SUCCESS: "success", + COMMFAIL: "comm fail", + VALIDATEFAIL: "validate fail", + COREFAIL: "core fail", + }; + + // ... 其他的配置 ... + + private constructor() {} + + static getInstance(): GlobalConfig { + if (!GlobalConfig.instance) { + GlobalConfig.instance = new GlobalConfig(); + } + return GlobalConfig.instance; + } +} + + + + diff --git a/core/src/controller/base.ts b/core/src/controller/base.ts new file mode 100644 index 0000000..6759cb5 --- /dev/null +++ b/core/src/controller/base.ts @@ -0,0 +1,205 @@ +import { + App, + CONTROLLER_KEY, + getClassMetadata, + Init, + Inject, + Provide, +} from "@midwayjs/decorator"; +import { GlobalConfig } from "../constant/global"; +import { ControllerOption, CurdOption } from "../decorator/controller"; +import { BaseService } from "../service/base"; +import { IMidwayApplication } from "@midwayjs/core"; +import { Context } from "@midwayjs/koa"; +import { TypeORMDataSourceManager } from "@midwayjs/typeorm"; + +/** + * 控制器基类 + */ +@Provide() +export abstract class BaseController { + @Inject("ctx") + baseCtx: Context; + + @Inject() + service: BaseService; + + @App() + baseApp: IMidwayApplication; + + curdOption: CurdOption; + + @Inject() + typeORMDataSourceManager: TypeORMDataSourceManager; + + connectionName; + + @Init() + async init() { + const option: ControllerOption = getClassMetadata(CONTROLLER_KEY, this); + const curdOption: CurdOption = option.curdOption; + this.curdOption = curdOption; + if (!this.curdOption) { + return; + } + // 操作之前 + await this.before(curdOption); + // 设置service + await this.setService(curdOption); + // 设置实体 + await this.setEntity(curdOption); + } + + private async before(curdOption: CurdOption) { + if (!curdOption?.before) { + return; + } + await curdOption.before(this.baseCtx, this.baseApp); + } + + /** + * 插入参数值 + * @param curdOption 配置 + */ + private async insertParam(curdOption: CurdOption) { + if (!curdOption?.insertParam) { + return; + } + this.baseCtx.request.body = { + // @ts-ignore + ...this.baseCtx.request.body, + ...(await curdOption.insertParam(this.baseCtx, this.baseApp)), + }; + } + + /** + * 设置实体 + * @param curdOption 配置 + */ + private async setEntity(curdOption: CurdOption) { + const entity = curdOption?.entity; + if (entity) { + const dataSourceName = + this.typeORMDataSourceManager.getDataSourceNameByModel(entity); + let entityModel = this.typeORMDataSourceManager + .getDataSource(dataSourceName) + .getRepository(entity); + this.service.setEntity(entityModel); + } + } + + /** + * 设置service + * @param curdOption + */ + private async setService(curdOption: CurdOption) { + if (curdOption.service) { + this.service = await this.baseCtx.requestContext.getAsync( + curdOption.service + ); + } + } + + /** + * 新增 + * @returns + */ + async add() { + // 插入参数 + await this.insertParam(this.curdOption); + const { body } = this.baseCtx.request; + return this.ok(await this.service.add(body)); + } + + /** + * 删除 + * @returns + */ + async delete() { + // @ts-ignore + const { ids } = this.baseCtx.request.body; + return this.ok(await this.service.delete(ids)); + } + + /** + * 更新 + * @returns + */ + async update() { + const { body } = this.baseCtx.request; + return this.ok(await this.service.update(body)); + } + + /** + * 分页查询 + * @returns + */ + async page() { + const { body } = this.baseCtx.request; + return this.ok( + await this.service.page( + body, + this.curdOption.pageQueryOp, + this.connectionName + ) + ); + } + + /** + * 列表查询 + * @returns + */ + async list() { + const { body } = this.baseCtx.request; + return this.ok( + await this.service.list( + body, + this.curdOption.listQueryOp, + this.connectionName + ) + ); + } + + /** + * 根据ID查询信息 + * @returns + */ + async info() { + const { id } = this.baseCtx.query; + return this.ok( + await this.service.info(id, this.curdOption.infoIgnoreProperty) + ); + } + + /** + * 成功返回 + * @param data 返回数据 + */ + ok(data?: any) { + const { RESCODE, RESMESSAGE } = GlobalConfig.getInstance(); + const res = { + code: RESCODE.SUCCESS, + message: RESMESSAGE.SUCCESS, + }; + if (data || data == 0) { + res["data"] = data; + } + return res; + } + + /** + * 失败返回 + * @param message + */ + fail(message?: string, code?: number) { + const { RESCODE, RESMESSAGE } = GlobalConfig.getInstance(); + return { + code: code ? code : RESCODE.COMMFAIL, + message: message + ? message + : code == RESCODE.VALIDATEFAIL + ? RESMESSAGE.VALIDATEFAIL + : RESMESSAGE.COMMFAIL, + }; + } +} diff --git a/core/src/decorator/cache.ts b/core/src/decorator/cache.ts new file mode 100644 index 0000000..f8195f0 --- /dev/null +++ b/core/src/decorator/cache.ts @@ -0,0 +1,8 @@ +import { createCustomMethodDecorator } from "@midwayjs/decorator"; + +// 装饰器内部的唯一 id +export const COOL_CACHE = "decorator:cool_cache"; + +export function CoolCache(ttl?: number): MethodDecorator { + return createCustomMethodDecorator(COOL_CACHE, ttl); +} diff --git a/core/src/decorator/controller.ts b/core/src/decorator/controller.ts new file mode 100644 index 0000000..98c863b --- /dev/null +++ b/core/src/decorator/controller.ts @@ -0,0 +1,208 @@ +import { ModuleConfig } from "./../interface"; +import { + Scope, + ScopeEnum, + saveClassMetadata, + saveModule, + CONTROLLER_KEY, + MiddlewareParamArray, + WEB_ROUTER_KEY, + attachClassMetadata, +} from "@midwayjs/decorator"; +import * as fs from "fs"; +import * as _ from "lodash"; +import * as os from "os"; +import location from "../util/location"; + +export type ApiTypes = "add" | "delete" | "update" | "page" | "info" | "list"; +// Crud配置 + +export interface CurdOption { + // 路由前缀,不配置默认是按Controller下的文件夹路径 + prefix?: string; + // curd api接口 + api: ApiTypes[]; + // 分页查询配置 + pageQueryOp?: QueryOp | Function; + // 非分页查询配置 + listQueryOp?: QueryOp | Function; + // 插入参数 + insertParam?: Function; + // 操作之前 + before?: Function; + // info 忽略返回属性 + infoIgnoreProperty?: string[]; + // 实体 + entity: any; + // 服务 + service?: any; + // api标签 + urlTag?: { + name: "ignoreToken" | string; + url: ApiTypes[]; + }; +} +export interface JoinOp { + // 实体 + entity: any; + // 别名 + alias: string; + // 关联条件 + condition: string; + // 关联类型 + type?: "innerJoin" | "leftJoin"; +} + +// 字段匹配 +export interface FieldEq { + // 字段 + column: string; + // 请求参数 + requestParam: string; +} +// 查询配置 +export interface QueryOp { + // 需要模糊查询的字段 + keyWordLikeFields?: string[]; + // 查询条件 + where?: Function; + // 查询字段 + select?: string[]; + // 字段相等 + fieldEq?: string[] | FieldEq[]; + // 添加排序条件 + addOrderBy?: {}; + // 关联配置 + join?: JoinOp[]; + // 其他条件 + extend?: Function; +} + +// Controller 配置 +export interface ControllerOption { + // crud配置 如果是字符串则为路由前缀,不配置默认是按Controller下的文件夹路径 + curdOption?: CurdOption & string; + // 路由配置 + routerOptions?: { + // 是否敏感 + sensitive?: boolean; + // 路由中间件 + middleware?: MiddlewareParamArray; + // 别名 + alias?: string[]; + // 描述 + description?: string; + // 标签名称 + tagName?: string; + }; +} + +// COOL的装饰器 +export function CoolController( + curdOption?: CurdOption | string, + routerOptions: { + sensitive?: boolean; + middleware?: MiddlewareParamArray; + description?: string; + tagName?: string; + ignoreGlobalPrefix?: boolean; + } = { middleware: [], sensitive: true } +): ClassDecorator { + return (target: any) => { + // 将装饰的类,绑定到该装饰器,用于后续能获取到 class + saveModule(CONTROLLER_KEY, target); + let prefix; + if (typeof curdOption === "string") { + prefix = curdOption; + } else { + prefix = curdOption?.prefix || ""; + } + // 如果不存在路由前缀,那么自动根据当前文件夹路径 + location.scriptPath(target).then(async (res: any) => { + const pathSps = res.path.split("."); + const paths = pathSps[pathSps.length - 2].split("/"); + const pathArr = []; + let module = null; + for (const path of paths.reverse()) { + if (path != "controller" && !module) { + pathArr.push(path); + } + if (path == "controller" && !paths.includes("modules")) { + break; + } + if (path == "controller" && paths.includes("modules")) { + module = "ready"; + } + if (module && path != "controller") { + module = `${path}`; + break; + } + } + if (module) { + pathArr.reverse(); + pathArr.splice(1, 0, module); + // 追加模块中间件 + let path = `${ + res.path.split(`modules/${module}`)[0] + }modules/${module}/config.${_.endsWith(res.path, "ts") ? "ts" : "js"}`; + if (os.type() == "Windows_NT") { + path = path.substr(1); + } + if (fs.existsSync(path)) { + const config: ModuleConfig = require(path).default(); + routerOptions.middleware = (config.middlewares || []).concat( + routerOptions.middleware || [] + ); + } + } + if (!prefix) { + prefix = `/${pathArr.join("/")}`; + } + saveMetadata(prefix, routerOptions, target, curdOption, module); + }); + }; +} + +export const apiDesc = { + add: "新增", + delete: "删除", + update: "修改", + page: "分页查询", + list: "列表查询", + info: "单个信息", +}; + +// 保存一些元数据信息,任意你希望存的东西 +function saveMetadata(prefix, routerOptions, target, curdOption, module) { + if (module && !routerOptions.tagName) { + routerOptions = routerOptions || {}; + routerOptions.tagName = module; + } + saveClassMetadata( + CONTROLLER_KEY, + { + prefix, + routerOptions, + curdOption, + module, + } as ControllerOption, + target + ); + // 追加CRUD路由 + if (!_.isEmpty(curdOption?.api)) { + curdOption?.api.forEach((path) => { + attachClassMetadata( + WEB_ROUTER_KEY, + { + path: `/${path}`, + requestMethod: path == "info" ? "get" : "post", + method: path, + summary: apiDesc[path], + description: "", + }, + target + ); + }); + Scope(ScopeEnum.Request)(target); + } +} diff --git a/core/src/decorator/event.ts b/core/src/decorator/event.ts new file mode 100644 index 0000000..e21b9db --- /dev/null +++ b/core/src/decorator/event.ts @@ -0,0 +1,42 @@ +import { + Scope, + ScopeEnum, + saveClassMetadata, + saveModule, + attachClassMetadata, +} from "@midwayjs/decorator"; + +export const COOL_CLS_EVENT_KEY = "decorator:cool:cls:event"; + +export function CoolEvent(): ClassDecorator { + return (target: any) => { + // 将装饰的类,绑定到该装饰器,用于后续能获取到 class + saveModule(COOL_CLS_EVENT_KEY, target); + // 保存一些元数据信息,任意你希望存的东西 + saveClassMetadata(COOL_CLS_EVENT_KEY, {}, target); + // 指定 IoC 容器创建实例的作用域,这里注册为请求作用域,这样能取到 ctx + Scope(ScopeEnum.Singleton)(target); + }; +} + +export const COOL_EVENT_KEY = "decorator:cool:event"; + +/** + * 事件 + * @param eventName + * @returns + */ +export function Event(eventName?: string): MethodDecorator { + return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { + // 将装饰的类,绑定到该装饰器,用于后续能获取到 class + attachClassMetadata( + COOL_EVENT_KEY, + { + eventName, + propertyKey, + descriptor, + }, + target + ); + }; +} diff --git a/core/src/decorator/index.ts b/core/src/decorator/index.ts new file mode 100644 index 0000000..2d2a3d3 --- /dev/null +++ b/core/src/decorator/index.ts @@ -0,0 +1,109 @@ +import { COOL_CACHE } from "./cache"; +import { CacheManager } from "@midwayjs/cache"; +import { + Init, + Inject, + JoinPoint, + MidwayDecoratorService, + Provide, + Scope, + ScopeEnum, +} from "@midwayjs/core"; +import { TypeORMDataSourceManager } from "@midwayjs/typeorm"; +import { CoolCommException } from "../exception/comm"; +import { COOL_TRANSACTION, TransactionOptions } from "./transaction"; +import * as md5 from "md5"; +import { CoolUrlTagData } from "../tag/data"; + +/** + * 装饰器 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolDecorator { + @Inject() + typeORMDataSourceManager: TypeORMDataSourceManager; + + @Inject() + decoratorService: MidwayDecoratorService; + + @Inject() + cacheManager: CacheManager; + + @Inject() + coolUrlTagData: CoolUrlTagData; + + @Init() + async init() { + // 事务 + await this.transaction(); + // 缓存 + await this.cache(); + // URL标签 + await this.coolUrlTagData.init(); + } + + /** + * 缓存 + */ + async cache() { + this.decoratorService.registerMethodHandler(COOL_CACHE, (options) => { + return { + around: async (joinPoint: JoinPoint) => { + const key = md5( + joinPoint.target.constructor.name + + joinPoint.methodName + + JSON.stringify(joinPoint.args) + ); + // 缓存有数据就返回 + let data: any = await this.cacheManager.get(key); + if (data) { + return JSON.parse(data); + } else { + // 执行原始方法 + data = await joinPoint.proceed(...joinPoint.args); + await this.cacheManager.set(key, JSON.stringify(data), { + ttl: options.metadata, + }); + } + return data; + }, + }; + }); + } + + /** + * 事务 + */ + async transaction() { + this.decoratorService.registerMethodHandler(COOL_TRANSACTION, (options) => { + return { + around: async (joinPoint: JoinPoint) => { + const option: TransactionOptions = options.metadata; + const dataSource = this.typeORMDataSourceManager.getDataSource( + option?.connectionName || "default" + ); + const queryRunner = dataSource.createQueryRunner(); + await queryRunner.connect(); + if (option && option.isolation) { + await queryRunner.startTransaction(option.isolation); + } else { + await queryRunner.startTransaction(); + } + let data; + try { + joinPoint.args.push(queryRunner); + data = await joinPoint.proceed(...joinPoint.args); + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw new CoolCommException(error.message); + } finally { + await queryRunner.release(); + } + return data; + }, + }; + }); + } +} diff --git a/core/src/decorator/tag.ts b/core/src/decorator/tag.ts new file mode 100644 index 0000000..a2a94ab --- /dev/null +++ b/core/src/decorator/tag.ts @@ -0,0 +1,50 @@ +import { saveClassMetadata, savePropertyDataToClass, saveModule } from "@midwayjs/decorator"; + +export const COOL_URL_TAG_KEY = "decorator:cool:url:tag"; + +export const COOL_METHOD_TAG_KEY = "decorator:cool:method:tag"; + +export enum TagTypes { + IGNORE_TOKEN = "ignoreToken", + IGNORE_SIGN = "ignoreSign", +} + +export interface CoolUrlTagConfig { + key: TagTypes | string; + value?: string[]; +} + +/** + * 打标记 + * @param data + * @returns + */ +export function CoolUrlTag(data?: CoolUrlTagConfig): ClassDecorator { + return (target: any) => { + // 将装饰的类,绑定到该装饰器,用于后续能获取到 class + saveModule(COOL_URL_TAG_KEY, target); + // 保存一些元数据信息,任意你希望存的东西 + saveClassMetadata(COOL_URL_TAG_KEY, data, target); + }; +} + + +/** + * 方法打标记 + * @param data + * @returns + */ +export function CoolTag(tag: TagTypes | string): MethodDecorator { + return (target, key, descriptor: PropertyDescriptor) => { + savePropertyDataToClass( + COOL_METHOD_TAG_KEY, + { + key, + tag + }, + target, + key + ); + return descriptor; + }; +} \ No newline at end of file diff --git a/core/src/decorator/transaction.ts b/core/src/decorator/transaction.ts new file mode 100644 index 0000000..5922dc3 --- /dev/null +++ b/core/src/decorator/transaction.ts @@ -0,0 +1,19 @@ +import { createCustomMethodDecorator } from "@midwayjs/decorator"; + +type IsolationLevel = + | "READ UNCOMMITTED" + | "READ COMMITTED" + | "REPEATABLE READ" + | "SERIALIZABLE"; + +export interface TransactionOptions { + connectionName?: string; + isolation?: IsolationLevel; +} + +// 装饰器内部的唯一 id +export const COOL_TRANSACTION = "decorator:cool_transaction"; + +export function CoolTransaction(option?: TransactionOptions): MethodDecorator { + return createCustomMethodDecorator(COOL_TRANSACTION, option); +} diff --git a/core/src/entity/base.ts b/core/src/entity/base.ts new file mode 100644 index 0000000..8a40e4c --- /dev/null +++ b/core/src/entity/base.ts @@ -0,0 +1,27 @@ +import { + Index, + UpdateDateColumn, + CreateDateColumn, + PrimaryGeneratedColumn, +} from "typeorm"; +import { CoolBaseEntity } from "./typeorm"; + +/** + * 模型基类 + */ +export abstract class BaseEntity extends CoolBaseEntity { + // 默认自增 + @PrimaryGeneratedColumn("increment", { + comment: "ID", + // type: "bigint", + }) + id: number; + + @Index() + @CreateDateColumn({ comment: "创建时间" }) + createTime: Date; + + @Index() + @UpdateDateColumn({ comment: "更新时间" }) + updateTime: Date; +} diff --git a/core/src/entity/mongo.ts b/core/src/entity/mongo.ts new file mode 100644 index 0000000..f17dec5 --- /dev/null +++ b/core/src/entity/mongo.ts @@ -0,0 +1,25 @@ +import { + Index, + UpdateDateColumn, + CreateDateColumn, + // @ts-ignore + ObjectID, + ObjectIdColumn, +} from "typeorm"; +import { CoolBaseEntity } from "./typeorm"; + +/** + * 模型基类 + */ +export abstract class BaseMongoEntity extends CoolBaseEntity { + @ObjectIdColumn({ comment: "id" }) + id: ObjectID; + + @Index() + @CreateDateColumn({ comment: "创建时间" }) + createTime: Date; + + @Index() + @UpdateDateColumn({ comment: "更新时间" }) + updateTime: Date; +} diff --git a/core/src/entity/typeorm.ts b/core/src/entity/typeorm.ts new file mode 100644 index 0000000..f9c1c8b --- /dev/null +++ b/core/src/entity/typeorm.ts @@ -0,0 +1,3 @@ +import { BaseEntity } from "typeorm"; + +export abstract class CoolBaseEntity extends BaseEntity {} diff --git a/core/src/event/index.ts b/core/src/event/index.ts new file mode 100644 index 0000000..02ae835 --- /dev/null +++ b/core/src/event/index.ts @@ -0,0 +1,43 @@ +import { + App, + getClassMetadata, + Init, + listModule, + Provide, + Scope, + ScopeEnum, +} from "@midwayjs/decorator"; +import * as Events from "events"; +import { IMidwayApplication } from "@midwayjs/core"; +import { COOL_CLS_EVENT_KEY, COOL_EVENT_KEY } from "../decorator/event"; + +/** + * 事件 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolEventManager extends Events { + @App() + app: IMidwayApplication; + + @Init() + async init() { + const eventModules = listModule(COOL_CLS_EVENT_KEY); + for (const module of eventModules) { + this.handlerEvent(module); + } + } + + async handlerEvent(module) { + const events = getClassMetadata(COOL_EVENT_KEY, module); + for (const event of events) { + const method = event.eventName ? event.eventName : event.propertyKey; + this.on(method, async (...args) => { + const moduleInstance = await this.app + .getApplicationContext() + .getAsync(module); + moduleInstance[event.propertyKey](...args); + }); + } + } +} diff --git a/core/src/exception/base.ts b/core/src/exception/base.ts new file mode 100644 index 0000000..177b46b --- /dev/null +++ b/core/src/exception/base.ts @@ -0,0 +1,13 @@ +/** + * 异常基类 + */ +export class BaseException extends Error { + status: number; + + constructor(name: string, code: number, message: string) { + super(message); + + this.name = name; + this.status = code; + } +} diff --git a/core/src/exception/comm.ts b/core/src/exception/comm.ts new file mode 100644 index 0000000..d2c9525 --- /dev/null +++ b/core/src/exception/comm.ts @@ -0,0 +1,16 @@ +import { GlobalConfig } from '../constant/global'; +import { BaseException } from './base'; + +/** + * 通用异常 + */ +export class CoolCommException extends BaseException { + constructor(message: string) { + const { RESCODE, RESMESSAGE } = GlobalConfig.getInstance(); + super( + 'CoolCommException', + RESCODE.COMMFAIL, + message ? message : RESMESSAGE.COMMFAIL + ); + } +} diff --git a/core/src/exception/core.ts b/core/src/exception/core.ts new file mode 100644 index 0000000..9dc3a7e --- /dev/null +++ b/core/src/exception/core.ts @@ -0,0 +1,16 @@ +import { GlobalConfig } from '../constant/global'; +import { BaseException } from './base'; + +/** + * 核心异常 + */ +export class CoolCoreException extends BaseException { + constructor(message: string) { + const { RESCODE, RESMESSAGE } = GlobalConfig.getInstance(); + super( + 'CoolCoreException', + RESCODE.COREFAIL, + message ? message : RESMESSAGE.COREFAIL + ); + } +} diff --git a/core/src/exception/filter.ts b/core/src/exception/filter.ts new file mode 100644 index 0000000..170ddc3 --- /dev/null +++ b/core/src/exception/filter.ts @@ -0,0 +1,21 @@ +import { ILogger } from '@midwayjs/core'; +import { Catch, Logger } from '@midwayjs/decorator'; +import { GlobalConfig } from '../constant/global'; + +/** + * 全局异常处理 + */ +@Catch() +export class CoolExceptionFilter { + @Logger() + coreLogger: ILogger; + + async catch(err) { + const { RESCODE } = GlobalConfig.getInstance(); + this.coreLogger.error(err); + return { + code: err.status || RESCODE.COMMFAIL, + message: err.message, + }; + } +} diff --git a/core/src/exception/validate.ts b/core/src/exception/validate.ts new file mode 100644 index 0000000..da023ca --- /dev/null +++ b/core/src/exception/validate.ts @@ -0,0 +1,16 @@ +import { GlobalConfig } from '../constant/global'; +import { BaseException } from './base'; + +/** + * 校验异常 + */ +export class CoolValidateException extends BaseException { + constructor(message: string) { + const { RESCODE, RESMESSAGE } = GlobalConfig.getInstance(); + super( + 'CoolValidateException', + RESCODE.VALIDATEFAIL, + message ? message : RESMESSAGE.VALIDATEFAIL + ); + } +} diff --git a/core/src/index.ts b/core/src/index.ts new file mode 100644 index 0000000..0d4523c --- /dev/null +++ b/core/src/index.ts @@ -0,0 +1,46 @@ +export { CoolConfiguration as Configuration } from "./configuration"; + +// 异常处理 +export * from "./exception/filter"; +export * from "./exception/core"; +export * from "./exception/base"; +export * from "./exception/comm"; +export * from "./exception/validate"; + +// entity +export * from "./entity/base"; +export * from "./entity/typeorm"; +export * from "./entity/mongo"; + +// service +export * from "./service/base"; + +// controller +export * from "./controller/base"; + +// 事件 +export * from "./event/index"; + +// 装饰器 +export * from "./decorator/controller"; +export * from "./decorator/cache"; +export * from "./decorator/event"; +export * from "./decorator/transaction"; +export * from "./decorator/tag"; +export * from "./decorator/index"; + +// rest +export * from "./rest/eps"; + +// tag +export * from "./tag/data"; + +// 模块 +export * from "./module/config"; +export * from "./module/import"; +export * from "./module/menu"; + +// 其他 +export * from "./interface"; +export * from "./util/func"; +export * from "./constant/global"; diff --git a/core/src/interface.ts b/core/src/interface.ts new file mode 100644 index 0000000..e4147cd --- /dev/null +++ b/core/src/interface.ts @@ -0,0 +1,390 @@ +import { MiddlewareParamArray } from "@midwayjs/core"; +import { AedesOptions } from "aedes"; +import { PublishPacket } from "packet"; + +/** + * 模块配置 + */ +export interface ModuleConfig { + /** 名称 */ + name: string; + /** 描述 */ + description: string; + /** 模块中间件 */ + middlewares?: MiddlewareParamArray; + /** 全局中间件 */ + globalMiddlewares?: MiddlewareParamArray; + /** 模块加载顺序,默认为0,值越大越优先加载 */ + order?: number; +} + +export interface CoolConfig { + /** 短信 */ + sms: CoolSmsConfig, + /** 是否自动导入数据库 */ + initDB?: boolean; + /** 是否自动导入模块菜单 */ + initMenu?: boolean; + // 实体配置 + // entity?: { + // primaryType: "uuid" | "increment" | "rowid" | "identity"; + // }; + /** crud配置 */ + crud?: { + /** 软删除 */ + softDelete: boolean; + /** 分页查询每页条数 */ + pageSize: number; + // 多租户 + // tenant: boolean; + }; + /** elasticsearch配置 */ + es?: { + nodes: string[]; + options?: any; + }; + /** pay */ + pay?: { + /** 微信支付 */ + wx?: CoolWxPayConfig; + /** 支付宝支付 */ + ali?: CoolAliPayConfig; + }; + /** rpc */ + rpc?: CoolRpcConfig; + /** redis */ + redis?: RedisConfig | RedisConfig[]; + /** 文件上传 */ + file?: { + /** 上传模式 */ + mode: MODETYPE; + /** 本地上传 文件地址前缀 */ + domain?: string; + /** oss */ + oss?: OSSConfig; + /** cos */ + cos?: COSConfig; + /** qiniu */ + qiniu?: QINIUConfig; + }; + /** IOT 配置 */ + iot?: CoolIotConfig; +} + +export interface CoolRpcConfig { + /** 服务名称 */ + name: string; + /** redis */ + redis: RedisConfig & RedisConfig[] & unknown; +} + +export interface RedisConfig { + /** host */ + host: string; + /** password */ + password: string; + /** port */ + port: number; + /** db */ + db: number; +} + +// 模式 +export enum MODETYPE { + /** 本地 */ + LOCAL = "local", + /** 云存储 */ + CLOUD = "cloud", + /** 其他 */ + OTHER = "other", +} + +export enum CLOUDTYPE { + /** 阿里云存储 */ + OSS = "oss", + /** 腾讯云存储 */ + COS = "cos", + /** 七牛云存储 */ + QINIU = "qiniu", +} + +/** + * 上传模式 + */ +export interface Mode { + /** 模式 */ + mode: MODETYPE; + /** 类型 */ + type: string; +} + +/** + * 模块配置 + */ +export interface CoolFileConfig { + /** 上传模式 */ + mode: MODETYPE; + /** 阿里云oss 配置 */ + oss: OSSConfig; + /** 腾讯云 cos配置 */ + cos: COSConfig; + /** 七牛云 配置 */ + qiniu: QINIUConfig; + /** 文件前缀 */ + domain: string; +} + +/** + * OSS 配置 + */ +export interface OSSConfig { + /** 阿里云accessKeyId */ + accessKeyId: string; + /** 阿里云accessKeySecret */ + accessKeySecret: string; + /** 阿里云oss的bucket */ + bucket: string; + /** 阿里云oss的endpoint */ + endpoint: string; + /** 阿里云oss的timeout */ + timeout: string; + /** 签名失效时间,毫秒 */ + expAfter?: number; + /** 文件最大的 size */ + maxSize?: number; + // host + host?: string; +} + +/** + * COS 配置 + */ +export interface COSConfig { + /** 腾讯云accessKeyId */ + accessKeyId: string; + /** 腾讯云accessKeySecret */ + accessKeySecret: string; + /** 腾讯云cos的bucket */ + bucket: string; + /** 腾讯云cos的区域 */ + region: string; + /** 腾讯云cos的公网访问地址 */ + publicDomain: string; + /** 上传持续时间 */ + durationSeconds?: number; + /** 允许操作(上传)的对象前缀 */ + allowPrefix?: string; + /** 密钥的权限列表 */ + allowActions?: string[]; +} + +export interface QINIUConfig { + /** 七牛云accessKeyId */ + accessKeyId: string; + /** 七牛云accessKeySecret */ + accessKeySecret: string; + /** 七牛云cos的bucket */ + bucket: string; + /** 七牛云cos的区域 */ + region: string; + /** 七牛云cos的公网访问地址 */ + publicDomain: string; + /** 上传地址 */ + uploadUrl?: string; + /** 上传fileKey */ + fileKey?: string; +} + +/** + * 微信支付配置 + */ +export interface CoolWxPayConfig { + /** 直连商户申请的公众号或移动应用appid。 */ + appid: string; + /** 商户号 */ + mchid: string; + /** 可选参数 证书序列号 */ + serial_no?: string; + /** 回调链接 */ + notify_url: string; + /** 公钥 */ + publicKey: Buffer; + /** 私钥 */ + privateKey: Buffer; + /** 可选参数 认证类型,目前为WECHATPAY2-SHA256-RSA2048 */ + authType?: string; + /** 可选参数 User-Agent */ + userAgent?: string; + /** 可选参数 APIv3密钥 */ + key?: string; +} + +/** + * 支付宝支付配置 + */ +export interface CoolAliPayConfig { + /** 支付回调地址 */ + notifyUrl: string; + /** 应用ID */ + appId: string; + /** + * 应用私钥字符串 + * RSA签名验签工具:https://docs.open.alipay.com/291/106097) + * 密钥格式一栏请选择 “PKCS1(非JAVA适用)” + */ + privateKey: string; + /** 签名类型 */ + signType?: "RSA2" | "RSA"; + /** 支付宝公钥(需要对返回值做验签时候必填) */ + alipayPublicKey?: string; + /** 网关 */ + gateway?: string; + /** 网关超时时间(单位毫秒,默认 5s) */ + timeout?: number; + /** 是否把网关返回的下划线 key 转换为驼峰写法 */ + camelcase?: boolean; + /** 编码(只支持 utf-8) */ + charset?: "utf-8"; + /** api版本 */ + version?: "1.0"; + urllib?: any; + /** 指定private key类型, 默认: PKCS1, PKCS8: PRIVATE KEY, PKCS1: RSA PRIVATE KEY */ + keyType?: "PKCS1" | "PKCS8"; + /** 应用公钥证书文件路径 */ + appCertPath?: string; + /** 应用公钥证书文件内容 */ + appCertContent?: string | Buffer; + /** 应用公钥证书sn */ + appCertSn?: string; + /** 支付宝根证书文件路径 */ + alipayRootCertPath?: string; + /** 支付宝根证书文件内容 */ + alipayRootCertContent?: string | Buffer; + /** 支付宝根证书sn */ + alipayRootCertSn?: string; + /** 支付宝公钥证书文件路径 */ + alipayPublicCertPath?: string; + /** 支付宝公钥证书文件内容 */ + alipayPublicCertContent?: string | Buffer; + /** 支付宝公钥证书sn */ + alipayCertSn?: string; + /** AES密钥,调用AES加解密相关接口时需要 */ + encryptKey?: string; + /** 服务器地址 */ + wsServiceUrl?: string; +} + +/** + * IOT配置 + */ +export interface CoolIotConfig { + /** MQTT服务端口 */ + port: number; + /** MQTT Websocket服务端口 */ + wsPort: number; + /** redis 配置 mqtt cluster下必须要配置 */ + redis?: { + /** host */ + host: string; + /** port */ + port: number; + /** password */ + password: string; + /** db */ + db: number; + }; + /** 发布消息配置 */ + publish?: PublishPacket; + /** 认证 */ + auth?: { + /** 用户 */ + username: string; + /** 密码 */ + password: string; + }; + /** 服务配置 */ + serve?: AedesOptions; +} + + +export interface CoolSmsConfig { + /** + * 阿里云短信配置 + */ + ali: CoolSmsAliConfig; + /** + * 腾讯云短信配置 + */ + tx: CoolTxConfig; + /** + * 云片短信配置 + */ + yp: CoolYpConfig; +} + +/** + * 阿里云配置 + */ +export interface CoolSmsAliConfig { + /** + * 阿里云accessKeyId + */ + accessKeyId: string; + /** + * 阿里云accessKeySecret + */ + accessKeySecret: string; + /** + * 签名,非必填,调用时可以传入 + */ + signName?: string; + /** + * 模板,非必填,调用时可以传入 + */ + template?: string; +} + +/** + * 腾讯云配置 + */ +export interface CoolTxConfig { + /** + * 应用ID + */ + appId: string; + /** + * 腾讯云secretId + */ + secretId: string; + /** + * 腾讯云secretKey + */ + secretKey: string; + /** + * 签名,非必填,调用时可以传入 + */ + signName?: string; + /** + * 模板,非必填,调用时可以传入 + */ + template?: string; +} + +/** + * 云片短信配置 + */ +export interface CoolYpConfig { + /** + * 云片apikey + */ + apikey: string; + /** + * 签名,非必填,调用时可以传入 + */ + signName?: string; + /** + * 模板,非必填,调用时可以传入 + */ + template?: string; +} + diff --git a/core/src/module/config.ts b/core/src/module/config.ts new file mode 100644 index 0000000..d3b510a --- /dev/null +++ b/core/src/module/config.ts @@ -0,0 +1,100 @@ +import { IMidwayApplication } from '@midwayjs/core'; +import { + ALL, + App, + Config, + Init, + Provide, + Scope, + ScopeEnum, +} from '@midwayjs/decorator'; +import * as fs from 'fs'; +import { CoolCoreException } from '../exception/core'; +import { ModuleConfig } from '../interface'; +import * as _ from 'lodash'; + +/** + * 模块配置 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolModuleConfig { + @App() + app: IMidwayApplication; + + @Config(ALL) + allConfig; + + modules; + + @Init() + async init() { + let modules = []; + // 模块路径 + const moduleBasePath = `${this.app.getBaseDir()}/modules/`; + if (!fs.existsSync(moduleBasePath)) { + return; + } + if (!this.allConfig['module']) { + this.allConfig['module'] = {}; + } + // 全局中间件 + const globalMiddlewareArr = []; + for (const module of fs.readdirSync(moduleBasePath)) { + const modulePath = `${moduleBasePath}/${module}`; + const dirStats = fs.statSync(modulePath); + if (dirStats.isDirectory()) { + const configPath = `${modulePath}/config.${ + this.app.getEnv() == 'local' ? 'ts' : 'js' + }`; + if (fs.existsSync(configPath)) { + const moduleConfig: ModuleConfig = require(configPath).default({ + app: this.app, + env: this.app.getEnv(), + }); + modules.push({ + order: moduleConfig.order || 0, + module: module, + }); + await this.moduleConfig(module, moduleConfig); + // 处理全局中间件 + if (!_.isEmpty(moduleConfig.globalMiddlewares)) { + globalMiddlewareArr.push({ + order: moduleConfig.order || 0, + data: moduleConfig.globalMiddlewares, + }); + } + } else { + throw new CoolCoreException(`模块【${module}】缺少config.ts配置文件`); + } + } + } + this.modules = _.orderBy(modules, ['order'], ['desc']).map(e => { + return e.module; + }); + await this.globalMiddlewareArr(globalMiddlewareArr); + } + + /** + * 模块配置 + * @param module 模块 + * @param config 配置 + */ + async moduleConfig(module, config) { + // 追加配置 + this.allConfig['module'][module] = config; + } + + /** + * 全局中间件 + * @param middleware 中间件 + */ + async globalMiddlewareArr(middlewares: any[]) { + middlewares = _.orderBy(middlewares, ['order'], ['desc']); + for (const middleware of middlewares) { + for (const item of middleware.data) { + this.app.getMiddleware().insertLast(item); + } + } + } +} diff --git a/core/src/module/import.ts b/core/src/module/import.ts new file mode 100644 index 0000000..5ce1fef --- /dev/null +++ b/core/src/module/import.ts @@ -0,0 +1,155 @@ +import { ILogger, IMidwayApplication } from "@midwayjs/core"; +import { + App, + Config, + Init, + Inject, + Logger, + Provide, + Scope, + ScopeEnum, +} from "@midwayjs/decorator"; +import { CoolCoreException } from "../exception/core"; +import * as Importer from "mysql2-import"; +import * as fs from "fs"; +import { CoolModuleConfig } from "./config"; +import * as path from "path"; +import { InjectDataSource, TypeORMDataSourceManager } from "@midwayjs/typeorm"; +import { DataSource } from "typeorm"; +import { CoolEventManager } from "../event"; +import { CoolModuleMenu } from "./menu"; + +/** + * 模块sql + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolModuleImport { + @Config("typeorm.dataSource") + ormConfig; + + @InjectDataSource("default") + defaultDataSource: DataSource; + + @Inject() + typeORMDataSourceManager: TypeORMDataSourceManager; + + @Config("cool") + coolConfig; + + @Logger() + coreLogger: ILogger; + + @Inject() + coolModuleConfig: CoolModuleConfig; + + @Inject() + coolEventManager: CoolEventManager; + + @App() + app: IMidwayApplication; + + @Inject() + coolModuleMenu: CoolModuleMenu; + + @Init() + async init() { + // 是否需要导入 + if (this.coolConfig.initDB) { + await this.checkDbVersion(); + const modules = this.coolModuleConfig.modules; + const importLockPath = path.join( + `${this.app.getBaseDir()}`, + "..", + "lock" + ); + if (!fs.existsSync(importLockPath)) { + fs.mkdirSync(importLockPath); + } + setTimeout(async () => { + for (const module of modules) { + const lockPath = path.join(importLockPath, module + ".sql.lock"); + if (!fs.existsSync(lockPath)) { + await this.initDataBase(module, lockPath); + } + } + this.coolEventManager.emit("onDBInit", {}); + this.coolModuleMenu.init(); + }, 2000); + } + } + + /** + * 导入数据库 + * @param module + * @param lockPath 锁定导入 + */ + async initDataBase(module: string, lockPath: string) { + // 模块路径 + const modulePath = `${this.app.getBaseDir()}/modules/${module}`; + // sql 路径 + const sqlPath = `${modulePath}/init.sql`; + // 延迟2秒再导入数据库 + if (fs.existsSync(sqlPath)) { + let second = 0; + const t = setInterval(() => { + this.coreLogger.info( + "\x1B[36m [cool:core] midwayjs cool core init " + + module + + " database... \x1B[0m" + ); + second++; + }, 1000); + const { host, username, password, database, charset, port } = this + .ormConfig?.default + ? this.ormConfig.default + : this.ormConfig; + const importer = new Importer({ + host, + password, + database, + charset, + port, + user: username, + }); + await importer + .import(sqlPath) + .then(async () => { + clearInterval(t); + this.coreLogger.info( + "\x1B[36m [cool:core] midwayjs cool core init " + + module + + " database complete \x1B[0m" + ); + fs.writeFileSync(lockPath, `time consuming:${second}s`); + }) + .catch((err) => { + clearTimeout(t); + this.coreLogger.error( + "\x1B[36m [cool:core] midwayjs cool core init " + + module + + " database err please manual import \x1B[0m" + ); + fs.writeFileSync(lockPath, `time consuming:${second}s`); + this.coreLogger.error(err); + this.coreLogger.error( + `自动初始化模块[${module}]数据库失败,尝试手动导入数据库` + ); + }); + } + } + + /** + * 检查数据库版本 + */ + async checkDbVersion() { + const versions = ( + await this.defaultDataSource.query("SELECT VERSION() AS version") + )[0].version.split("."); + if ((versions[0] == 5 && versions[1] < 7) || versions[0] < 5) { + throw new CoolCoreException( + "数据库不满足要求:mysql>=5.7,请升级数据库版本" + ); + } + } +} diff --git a/core/src/module/menu.ts b/core/src/module/menu.ts new file mode 100644 index 0000000..fd864ab --- /dev/null +++ b/core/src/module/menu.ts @@ -0,0 +1,76 @@ +import { App, Config, ILogger, IMidwayApplication, Inject, Logger, Provide, Scope, ScopeEnum } from "@midwayjs/core"; +import * as fs from "fs"; +import { CoolModuleConfig } from "./config"; +import * as path from "path"; +import { CoolConfig } from "../interface"; +import { CoolEventManager } from "../event"; + +/** + * 菜单 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolModuleMenu { + + @Inject() + coolModuleConfig: CoolModuleConfig; + + @Config("cool") + coolConfig: CoolConfig; + + @App() + app: IMidwayApplication; + + @Logger() + coreLogger: ILogger; + + @Inject() + coolEventManager: CoolEventManager; + + async init() { + // 是否需要导入 + if (this.coolConfig.initMenu) { + const modules = this.coolModuleConfig.modules; + const importLockPath = path.join( + `${this.app.getBaseDir()}`, + "..", + "lock" + ); + if (!fs.existsSync(importLockPath)) { + fs.mkdirSync(importLockPath); + } + for (const module of modules) { + const lockPath = path.join(importLockPath, module + ".menu.lock"); + if (!fs.existsSync(lockPath)) { + await this.importMenu(module, lockPath); + } + } + this.coolEventManager.emit("onMenuInit", {}); + } + } + + /** + * 导入菜单 + * @param module + * @param lockPath + */ + async importMenu(module: string, lockPath: string){ + // 模块路径 + const modulePath = `${this.app.getBaseDir()}/modules/${module}`; + // json 路径 + const menuPath = `${modulePath}/menu.json`; + // 导入 + if (fs.existsSync(menuPath)) { + const data = fs.readFileSync(menuPath); + try { + this.coolEventManager.emit("onMenuImport", module, JSON.parse(data.toString())); + fs.writeFileSync(lockPath, data); + } catch (error) { + this.coreLogger.error(error); + this.coreLogger.error( + `自动初始化模块[${module}]菜单失败,请检查对应的数据结构是否正确` + ); + } + } + } +} \ No newline at end of file diff --git a/core/src/package.json b/core/src/package.json new file mode 100644 index 0000000..4534630 --- /dev/null +++ b/core/src/package.json @@ -0,0 +1,54 @@ +{ + "name": "@cool-midway/core", + "version": "7.0.0-beta1", + "description": "", + "main": "index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [ + "cool", + "cool-admin", + "cooljs" + ], + "readme": "README.md", + "author": "COOL", + "files": [ + "**/*.js", + "**/*.d.ts", + "index.d.ts" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "devDependencies": { + "@midwayjs/cli": "1.3.21", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/koa": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@midwayjs/typeorm": "^3.9.0", + "@types/jest": "^29.2.4", + "@types/node": "^18.11.15", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typeorm": "^0.3.11", + "typescript": "~4.9.4" + }, + "dependencies": { + "@midwayjs/cache": "^3.9.0", + "lodash": "^4.17.21", + "md5": "^2.3.0", + "moment": "^2.29.4", + "mysql2-import": "^5.0.22", + "sqlstring": "^2.3.3" + } +} diff --git a/core/src/rest/eps.ts b/core/src/rest/eps.ts new file mode 100644 index 0000000..623b733 --- /dev/null +++ b/core/src/rest/eps.ts @@ -0,0 +1,124 @@ +import { + CONTROLLER_KEY, + getClassMetadata, + listModule, + Provide, + Scope, + ScopeEnum, +} from "@midwayjs/decorator"; +import * as _ from "lodash"; +import { Inject, MidwayWebRouterService } from "@midwayjs/core"; +import { TypeORMDataSourceManager } from "@midwayjs/typeorm"; + +/** + * 实体路径 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolEps { + admin = {}; + + app = {}; + + @Inject() + midwayWebRouterService: MidwayWebRouterService; + + @Inject() + typeORMDataSourceManager: TypeORMDataSourceManager; + + // @Init() + async init() { + const entitys = await this.entity(); + const controllers = await this.controller(); + const routers = await this.router(); + const adminArr = []; + const appArr = []; + for (const controller of controllers) { + const { prefix, module, curdOption } = controller; + const name = curdOption?.entity?.name; + (_.startsWith(prefix, "/admin/") ? adminArr : appArr).push({ + module, + api: routers[prefix], + name, + columns: entitys[name] || [], + prefix, + }); + } + this.admin = _.groupBy(adminArr, "module"); + this.app = _.groupBy(appArr, "module"); + } + + /** + * 所有controller + * @returns + */ + async controller() { + const result = []; + const controllers = listModule(CONTROLLER_KEY); + for (const controller of controllers) { + result.push(getClassMetadata(CONTROLLER_KEY, controller)); + } + + return result; + } + + /** + * 所有路由 + * @returns + */ + async router() { + return _.groupBy( + (await await this.midwayWebRouterService.getFlattenRouterTable()).map( + (item) => { + return { + method: item.requestMethod, + path: item.url, + summary: item.summary, + dts: {}, + tag: "", + prefix: item.prefix, + }; + } + ), + "prefix" + ); + } + + /** + * 所有实体 + * @returns + */ + async entity() { + const result = {}; + const dataSourceNames = this.typeORMDataSourceManager.getDataSourceNames(); + for (const dataSourceName of dataSourceNames) { + const entityMetadatas = await this.typeORMDataSourceManager.getDataSource( + dataSourceName + ).entityMetadatas; + for (const entityMetadata of entityMetadatas) { + const commColums = []; + let columns = entityMetadata.columns; + columns = _.filter( + columns.map((e) => { + return { + propertyName: e.propertyName, + type: + typeof e.type == "string" ? e.type : e.type.name.toLowerCase(), + length: e.length, + comment: e.comment, + nullable: e.isNullable, + }; + }), + (o) => { + if (["createTime", "updateTime"].includes(o.propertyName)) { + commColums.push(o); + } + return o && !["createTime", "updateTime"].includes(o.propertyName); + } + ).concat(commColums); + result[entityMetadata.name] = columns; + } + } + return result; + } +} diff --git a/core/src/service/base.ts b/core/src/service/base.ts new file mode 100644 index 0000000..7dec3ff --- /dev/null +++ b/core/src/service/base.ts @@ -0,0 +1,526 @@ +import { Init, Provide, Inject, App, Config } from "@midwayjs/decorator"; +import { CoolValidateException } from "../exception/validate"; +import { ERRINFO, EVENT } from "../constant/global"; +import { Application, Context } from "@midwayjs/koa"; +import * as SqlString from "sqlstring"; +import { CoolConfig } from "../interface"; +import { TypeORMDataSourceManager } from "@midwayjs/typeorm"; +import { Brackets, In, Repository, SelectQueryBuilder } from "typeorm"; +import { QueryOp } from "../decorator/controller"; +import * as _ from "lodash"; +import { CoolEventManager } from "../event"; + +/** + * 服务基类 + */ +@Provide() +export abstract class BaseService { + // 分页配置 + @Config("cool") + private _coolConfig: CoolConfig; + + // 模型 + protected entity: Repository; + + protected sqlParams; + + @Inject() + typeORMDataSourceManager: TypeORMDataSourceManager; + + @Inject() + coolEventManager: CoolEventManager; + + // 设置模型 + setEntity(entity: any) { + this.entity = entity; + } + + // 设置请求上下文 + setCtx(ctx: Context) { + this.baseCtx = ctx; + } + + @App() + baseApp: Application; + + // 设置应用对象 + setApp(app: Application) { + this.baseApp = app; + } + + @Inject("ctx") + baseCtx: Context; + + // 初始化 + @Init() + init() { + this.sqlParams = []; + } + + /** + * 设置sql + * @param condition 条件是否成立 + * @param sql sql语句 + * @param params 参数 + */ + setSql(condition, sql, params) { + let rSql = false; + if (condition || (condition === 0 && condition !== "")) { + rSql = true; + this.sqlParams = this.sqlParams.concat(params); + } + return rSql ? sql : ""; + } + + /** + * 获得查询个数的SQL + * @param sql + */ + getCountSql(sql) { + sql = sql + .replace(new RegExp("LIMIT", "gm"), "limit ") + .replace(new RegExp("\n", "gm"), " "); + if (sql.includes("limit")) { + const sqlArr = sql.split("limit "); + sqlArr.pop(); + sql = sqlArr.join("limit "); + } + return `select count(*) as count from (${sql}) a`; + } + + /** + * 参数安全性检查 + * @param params + */ + async paramSafetyCheck(params) { + const lp = params.toLowerCase(); + return !( + lp.indexOf("update ") > -1 || + lp.indexOf("select ") > -1 || + lp.indexOf("delete ") > -1 || + lp.indexOf("insert ") > -1 + ); + } + + /** + * 原生查询 + * @param sql + * @param params + * @param connectionName + */ + async nativeQuery(sql, params?, connectionName?) { + if (_.isEmpty(params)) { + params = this.sqlParams; + } + let newParams = []; + newParams = newParams.concat(params); + this.sqlParams = []; + for (const param of newParams) { + SqlString.escape(param); + } + return await this.getOrmManager(connectionName).query(sql, newParams || []); + } + + /** + * 获得ORM管理 + * @param connectionName 连接名称 + */ + getOrmManager(connectionName = "default") { + return this.typeORMDataSourceManager.getDataSource(connectionName); + } + + /** + * 操作entity获得分页数据,不用写sql + * @param find QueryBuilder + * @param query + * @param autoSort + * @param connectionName + */ + async entityRenderPage( + find: SelectQueryBuilder, + query, + autoSort = true + ) { + const { + size = this._coolConfig.crud.pageSize, + page = 1, + order = "createTime", + sort = "desc", + isExport = false, + maxExportLimit, + } = query; + const count = await find.getCount(); + let dataFind: SelectQueryBuilder; + if (isExport && maxExportLimit > 0) { + dataFind = find.limit(maxExportLimit); + } else { + dataFind = find.offset((page - 1) * size).limit(size); + } + if (autoSort) { + find.addOrderBy(order, sort.toUpperCase()); + } + return { + list: await dataFind.getMany(), + pagination: { + page: parseInt(page), + size: parseInt(size), + total: count, + }, + }; + } + + /** + * 执行SQL并获得分页数据 + * @param sql 执行的sql语句 + * @param query 分页查询条件 + * @param autoSort 是否自动排序 + * @param connectionName 连接名称 + */ + async sqlRenderPage(sql, query, autoSort = true, connectionName?) { + const { + size = this._coolConfig.crud.pageSize, + page = 1, + order = "createTime", + sort = "desc", + isExport = false, + maxExportLimit, + } = query; + if (order && sort && autoSort) { + if (!(await this.paramSafetyCheck(order + sort))) { + throw new CoolValidateException("非法传参~"); + } + sql += ` ORDER BY ${SqlString.escapeId(order)} ${this.checkSort(sort)}`; + } + if (isExport && maxExportLimit > 0) { + this.sqlParams.push(parseInt(maxExportLimit)); + sql += " LIMIT ? "; + } + if (!isExport) { + this.sqlParams.push((page - 1) * size); + this.sqlParams.push(parseInt(size)); + sql += " LIMIT ?,? "; + } + + let params = []; + params = params.concat(this.sqlParams); + const result = await this.nativeQuery(sql, params, connectionName); + const countResult = await this.nativeQuery( + this.getCountSql(sql), + params, + connectionName + ); + return { + list: result, + pagination: { + page: parseInt(page), + size: parseInt(size), + total: parseInt(countResult[0] ? countResult[0].count : 0), + }, + }; + } + + /** + * 检查排序 + * @param sort 排序 + * @returns + */ + private checkSort(sort) { + if (!["desc", "asc"].includes(sort.toLowerCase())) { + throw new CoolValidateException("sort 非法传参~"); + } + return sort; + } + + /** + * 获得单个ID + * @param id ID + * @param infoIgnoreProperty 忽略返回属性 + */ + async info(id: any, infoIgnoreProperty?: string[]) { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + if (!id) { + throw new CoolValidateException(ERRINFO.NOID); + } + const info = await this.entity.findOneBy({ id }); + if (info && infoIgnoreProperty) { + for (const property of infoIgnoreProperty) { + delete info[property]; + } + } + return info; + } + + /** + * 删除 + * @param ids 删除的ID集合 如:[1,2,3] 或者 1,2,3 + */ + async delete(ids: any) { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + await this.modifyBefore(ids, "delete"); + if (ids instanceof String) { + ids = ids.split(","); + } + // 启动软删除发送事件 + if (this._coolConfig.crud?.softDelete) { + this.softDelete(ids); + } + await this.entity.delete(ids); + await this.modifyAfter(ids, "delete"); + } + + /** + * 软删除 + * @param ids 删除的ID数组 + * @param entity 实体 + */ + async softDelete(ids: number[], entity?: Repository) { + const data = await this.entity.find({ + where: { + id: In(ids), + }, + }); + if (_.isEmpty(data)) return; + const _entity = entity ? entity : this.entity; + const params = { + data, + ctx: this.baseCtx, + entity: _entity, + }; + if (data.length > 0) { + this.coolEventManager.emit(EVENT.SOFT_DELETE, params); + } + } + + /** + * 修改 + * @param param 数据 + */ + async update(param: any) { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + await this.modifyBefore(param, "update"); + if (!param.id && !(param instanceof Array)) + throw new CoolValidateException(ERRINFO.NOID); + await this.addOrUpdate(param); + await this.modifyAfter(param, "update"); + } + + /** + * 新增 + * @param param 数据 + */ + async add(param: any | any[]): Promise { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + await this.modifyBefore(param, "add"); + await this.addOrUpdate(param); + await this.modifyAfter(param, "add"); + return { + id: + param instanceof Array + ? param.map((e) => { + return e.id ? e.id : e._id; + }) + : param.id + ? param.id + : param._id, + }; + } + + /** + * 新增|修改 + * @param param 数据 + */ + async addOrUpdate(param: any | any[]) { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + delete param.createTime; + if (param.id) { + param.updateTime = new Date(); + await this.entity.save(param); + } else { + param.createTime = new Date(); + param.updateTime = new Date(); + await this.entity.save(param); + } + } + + /** + * 非分页查询 + * @param query 查询条件 + * @param option 查询配置 + * @param connectionName 连接名 + */ + async list(query, option, connectionName?): Promise { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + const sql = await this.getOptionFind(query, option); + return this.nativeQuery(sql, [], connectionName); + } + + /** + * 分页查询 + * @param query 查询条件 + * @param option 查询配置 + * @param connectionName 连接名 + */ + async page(query, option, connectionName?) { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + const sql = await this.getOptionFind(query, option); + return this.sqlRenderPage(sql, query, false, connectionName); + } + + /** + * 构建查询配置 + * @param query 前端查询 + * @param option + */ + private async getOptionFind(query, option: QueryOp) { + let { order = "createTime", sort = "desc", keyWord = "" } = query; + const sqlArr = ["SELECT"]; + const selects = ["a.*"]; + const find = this.entity.createQueryBuilder("a"); + if (option) { + if (typeof option == "function") { + // @ts-ignore + option = await option(this.baseCtx, this.baseApp); + } + // 判断是否有关联查询,有的话取个别名 + if (!_.isEmpty(option.join)) { + for (const item of option.join) { + selects.push(`${item.alias}.*`); + find[item.type || "leftJoin"]( + item.entity, + item.alias, + item.condition + ); + } + } + // 默认条件 + if (option.where) { + const wheres = + typeof option.where == "function" + ? await option.where(this.baseCtx, this.baseApp) + : option.where; + if (!_.isEmpty(wheres)) { + for (const item of wheres) { + if ( + item.length == 2 || + (item.length == 3 && + (item[2] || (item[2] === 0 && item[2] != ""))) + ) { + for (const key in item[1]) { + this.sqlParams.push(item[1][key]); + } + find.andWhere(item[0], item[1]); + } + } + } + } + // 附加排序 + if (!_.isEmpty(option.addOrderBy)) { + for (const key in option.addOrderBy) { + if (order && order == key) { + sort = option.addOrderBy[key].toUpperCase(); + } + find.addOrderBy( + SqlString.escapeId(key), + this.checkSort(option.addOrderBy[key].toUpperCase()) + ); + } + } + // 关键字模糊搜索 + if (keyWord || (keyWord == 0 && keyWord != "")) { + keyWord = `%${keyWord}%`; + find.andWhere( + new Brackets((qb) => { + const keyWordLikeFields = option.keyWordLikeFields; + for (let i = 0; i < option.keyWordLikeFields?.length || 0; i++) { + qb.orWhere(`${keyWordLikeFields[i]} like :keyWord`, { + keyWord, + }); + this.sqlParams.push(keyWord); + } + }) + ); + } + // 筛选字段 + if (!_.isEmpty(option.select)) { + sqlArr.push(option.select.join(",")); + find.select(option.select); + } else { + sqlArr.push(selects.join(",")); + } + // 字段全匹配 + if (!_.isEmpty(option.fieldEq)) { + for (const key of option.fieldEq) { + const c = {}; + // 单表字段无别名的情况下操作 + if (typeof key === "string") { + if (query[key] || (query[key] == 0 && query[key] == "")) { + c[key] = query[key]; + const eq = query[key] instanceof Array ? "in" : "="; + if (eq === "in") { + find.andWhere(`${key} ${eq} (:${key})`, c); + } else { + find.andWhere(`${key} ${eq} :${key}`, c); + } + this.sqlParams.push(query[key]); + } + } else { + if ( + query[key.requestParam] || + (query[key.requestParam] == 0 && query[key.requestParam] !== "") + ) { + c[key.column] = query[key.requestParam]; + const eq = query[key.requestParam] instanceof Array ? "in" : "="; + if (eq === "in") { + find.andWhere(`${key.column} ${eq} (:${key.column})`, c); + } else { + find.andWhere(`${key.column} ${eq} :${key.column}`, c); + } + this.sqlParams.push(query[key.requestParam]); + } + } + } + } + } else { + sqlArr.push(selects.join(",")); + } + // 接口请求的排序 + if (sort && order) { + const sorts = sort.toUpperCase().split(","); + const orders = order.split(","); + if (sorts.length != orders.length) { + throw new CoolValidateException(ERRINFO.SORTFIELD); + } + for (const i in sorts) { + find.addOrderBy( + SqlString.escapeId(orders[i]), + this.checkSort(sorts[i]) + ); + } + } + if (option?.extend) { + await option?.extend(find, this.baseCtx, this.baseApp); + } + const sqls = find.getSql().split("FROM"); + sqlArr.push("FROM"); + sqlArr.push(sqls[1]); + return sqlArr.join(" "); + } + + /** + * 新增|修改|删除 之后的操作 + * @param data 对应数据 + */ + async modifyAfter( + data: any, + type: "delete" | "update" | "add" + ): Promise {} + + /** + * 新增|修改|删除 之前的操作 + * @param data 对应数据 + */ + async modifyBefore( + data: any, + type: "delete" | "update" | "add" + ): Promise {} +} diff --git a/core/src/tag/data.ts b/core/src/tag/data.ts new file mode 100644 index 0000000..d01b32f --- /dev/null +++ b/core/src/tag/data.ts @@ -0,0 +1,63 @@ +import { COOL_METHOD_TAG_KEY, CoolUrlTagConfig } from './../decorator/tag'; +import { + CONTROLLER_KEY, + getClassMetadata, + listPropertyDataFromClass, + listModule, + Provide, + Scope, + ScopeEnum, + WEB_ROUTER_KEY, +} from '@midwayjs/decorator'; +import { COOL_URL_TAG_KEY } from '../decorator/tag'; +import * as _ from 'lodash'; + +/** + * URL标签 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolUrlTagData { + data = {}; + + async init() { + const tags = listModule(COOL_URL_TAG_KEY); + for (const controller of tags) { + // class的标记 + const controllerOption = getClassMetadata(CONTROLLER_KEY, controller); + const tagOption: CoolUrlTagConfig = getClassMetadata( + COOL_URL_TAG_KEY, + controller + ); + if(tagOption?.key){ + const data: string[] = this.data[tagOption.key] || []; + this.data[tagOption.key] = _.uniq(data.concat( + (tagOption?.value || []).map(e => { + return controllerOption.prefix + '/' + e; + })) + ); + } + // 方法标记 + const listPropertyMetas = listPropertyDataFromClass(COOL_METHOD_TAG_KEY, controller); + const requestMetas = getClassMetadata(WEB_ROUTER_KEY, controller); + for (const propertyMeta of listPropertyMetas) { + const _data = this.data[propertyMeta.tag] || []; + const requestMeta = _.find(requestMetas, { method: propertyMeta.key }) + if(requestMeta){ + this.data[propertyMeta.tag] = _.uniq(_data.concat( + controllerOption.prefix + requestMeta.path + )) + } + } + } + } + + /** + * 根据键获得 + * @param key + * @returns + */ + byKey(key: string): string[] { + return this.data[key]; + } +} diff --git a/core/src/util/func.ts b/core/src/util/func.ts new file mode 100644 index 0000000..f796bbc --- /dev/null +++ b/core/src/util/func.ts @@ -0,0 +1,27 @@ +import { ILogger } from '@midwayjs/core'; +import { Init, Logger, Provide, Scope, ScopeEnum } from '@midwayjs/decorator'; +import * as moment from 'moment'; + +/** + * 常用函数处理 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class FuncUtil { + @Logger() + coreLogger: ILogger; + + @Init() + async init() { + Date.prototype.toJSON = function () { + return moment(this).format('YYYY-MM-DD HH:mm:ss'); + }; + // 新增String支持replaceAll方法 + String.prototype['replaceAll'] = function (s1, s2) { + return this.replace(new RegExp(s1, 'gm'), s2); + }; + this.coreLogger.info( + '\x1B[36m [cool:core] midwayjs cool core func handler \x1B[0m' + ); + } +} diff --git a/core/src/util/location.ts b/core/src/util/location.ts new file mode 100644 index 0000000..5558180 --- /dev/null +++ b/core/src/util/location.ts @@ -0,0 +1,95 @@ +import { Session } from 'inspector'; +import { v1 as uuid } from 'uuid'; +import * as util from 'util'; + +/** + * Location 工具类 + */ +class LocationUtil { + static instance = null; + + session: Session; + + PREFIX = '__functionLocation__'; + + scripts = {}; + + post$ = null; + + constructor() { + if (!LocationUtil.instance) { + this.init(); + LocationUtil.instance = this; + } + return LocationUtil.instance; + } + + init() { + if (!global[this.PREFIX]) { + global[this.PREFIX] = {}; + } + if (this.session) { + return; + } + this.session = new Session(); + this.session.connect(); + this.post$ = util.promisify(this.session.post).bind(this.session); + this.session.on('Debugger.scriptParsed', res => { + this.scripts[res.params.scriptId] = res.params; + LocationUtil.instance = this; + }); + this.post$('Debugger.enable'); + LocationUtil.instance = this; + } + + /** + * 获得脚本位置 + * @param target + */ + async scriptPath(target: any) { + const id = uuid(); + global[this.PREFIX][id] = target; + const evaluated = await this.post$('Runtime.evaluate', { + expression: `global['${this.PREFIX}']['${id}']`, + objectGroup: this.PREFIX, + }); + const properties = await this.post$('Runtime.getProperties', { + objectId: evaluated.result.objectId, + }); + const location = properties.internalProperties.find( + prop => prop.name === '[[FunctionLocation]]' + ); + const script = this.scripts[location.value.value.scriptId]; + delete global[this.PREFIX][id]; + let source = decodeURI(script.url); + if (!source.startsWith('file://')) { + source = `file://${source}`; + } + return { + column: location.value.value.columnNumber + 1, + line: location.value.value.lineNumber + 1, + path: source.substr(7), + source, + }; + } + + /** + * 清除 + */ + async clean() { + if (this.session) { + await this.post$('Runtime.releaseObjectGroup', { + objectGroup: this.PREFIX, + }); + this.session.disconnect(); + } + + this.session = null; + this.post$ = null; + this.scripts = null; + delete global[this.PREFIX]; + LocationUtil.instance = null; + } +} + +export default new LocationUtil(); diff --git a/core/tsconfig.json b/core/tsconfig.json new file mode 100644 index 0000000..9bb4ab7 --- /dev/null +++ b/core/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "inlineSourceMap":false, + "noImplicitThis": true, + "noUnusedLocals": true, + "stripInternal": true, + "skipLibCheck": true, + "noImplicitReturns": false, + "pretty": true, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/es/.editorconfig b/es/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/es/.editorconfig @@ -0,0 +1,11 @@ +# 🎨 editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/es/.eslintrc.json b/es/.eslintrc.json new file mode 100644 index 0000000..aad9755 --- /dev/null +++ b/es/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "extends": "./node_modules/mwts/", + "ignorePatterns": [ + "node_modules", + "dist", + "test", + "jest.config.js", + "typings", + "public/**/**", + "view/**/**" + ], + "env": { + "jest": true + }, + "rules": { + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/ban-ts-comment": "off", + "node/no-extraneous-import": "off", + "no-empty": "off", + "node/no-extraneous-require": "off", + "eqeqeq": "off", + "node/no-unsupported-features/node-builtins": "off", + "@typescript-eslint/ban-types": "off", + "no-control-regex": "off", + "prefer-const": "off" + } +} \ No newline at end of file diff --git a/es/.gitignore b/es/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/es/.gitignore @@ -0,0 +1,15 @@ +logs/ +npm-debug.log +yarn-error.log +node_modules/ +package-lock.json +yarn.lock +coverage/ +dist/ +.idea/ +run/ +.DS_Store +*.sw* +*.un~ +.tsbuildinfo +.tsbuildinfo.* diff --git a/es/.prettierrc.js b/es/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/es/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/es/README.md b/es/README.md new file mode 100644 index 0000000..610c3db --- /dev/null +++ b/es/README.md @@ -0,0 +1,3 @@ +# cool-admin + +https://cool-js.com diff --git a/es/index.d.ts b/es/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/es/index.d.ts @@ -0,0 +1,10 @@ +export * from './dist/index'; + +declare module '@midwayjs/core/dist/interface' { + interface MidwayConfig { + book?: PowerPartial<{ + a: number; + b: string; + }>; + } +} diff --git a/es/jest.config.js b/es/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/es/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], + setupFilesAfterEnv: ['./jest.setup.js'] +}; diff --git a/es/jest.setup.js b/es/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/es/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/es/package.json b/es/package.json new file mode 100644 index 0000000..a3f9236 --- /dev/null +++ b/es/package.json @@ -0,0 +1,43 @@ +{ + "name": "@cool-midway/es", + "version": "6.0.2", + "description": "cool-js.com elasticsearch", + "main": "dist/index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [ + "cool", + "cool-admin", + "cooljs" + ], + "author": "COOL", + "readme": "README.md", + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts", + "index.d.ts" + ], + "license": "MIT", + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^2.0.9", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@types/jest": "^29.2.5", + "@types/node": "^18.11.18", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typescript": "^4.9.4" + }, + "dependencies": { + "@elastic/elasticsearch": "^8.5.0" + } +} diff --git a/es/src/base.ts b/es/src/base.ts new file mode 100644 index 0000000..f6d83af --- /dev/null +++ b/es/src/base.ts @@ -0,0 +1,615 @@ +import { CoolEventManager } from '@cool-midway/core'; +import { Client } from '@elastic/elasticsearch'; +import { WaitForActiveShards } from '@elastic/elasticsearch/lib/api/types'; +import { Inject, Logger } from '@midwayjs/decorator'; +import { ILogger } from '@midwayjs/logger'; +import { EsConfig } from '.'; + +/** + * Es索引基类 + */ +export class BaseEsIndex { + // 索引 + public index: string; + // es客户端 + public client: Client; + // 日志 + @Logger() + coreLogger: ILogger; + // 事件 + @Inject('cool:coolEventManager') + coolEventManager: CoolEventManager; + + /** + * 设置索引 + * @param index + */ + setIndex(index: string) { + this.index = index; + } + + /** + * 处理es数据变更事件,主要用于同步数据 + * @param method + * @param data + */ + async handleDataChange(index, method, data) { + this.index = index; + const { + id, + ids, + bodys, + body, + type, + refresh, + waitForActiveShards, + properties, + config, + } = data; + switch (method) { + case 'upsert': + await this.upsert(body, refresh, waitForActiveShards); + break; + case 'batchIndex': + await this.batchIndex(bodys, type, refresh, waitForActiveShards); + break; + case 'deleteById': + await this.deleteById(id, refresh, waitForActiveShards); + break; + case 'deleteByIds': + await this.deleteByIds(ids, refresh, waitForActiveShards); + break; + case 'deleteByQuery': + await this.deleteByQuery(body, refresh, waitForActiveShards); + break; + case 'updateById': + await this.updateById(body, refresh, waitForActiveShards); + break; + case 'updateByQuery': + await this.updateByQuery(body, refresh, waitForActiveShards); + break; + case 'createIndex': + await this.updateByQuery(properties, config); + break; + } + } + + /** + * 数据更新事件 + * @param method + * @param data + */ + async esDataChange(method, data) { + this.coolEventManager?.emit('esDataChange', this.index, method, data); + } + + /** + * 设置客户端 + * @param client + */ + setClient(client: Client) { + this.client = client; + } + + /** + * 对象转body + * @param condition + */ + async objToBody(condition: any){ + const body = { + query: { + bool: { + must: [], + }, + } + } + for(const key in condition){ + body.query.bool.must.push({ + term: { + [key]: condition[key] + } + }) + } + return body; + } + + /** + * 按字段值查找 + * @param condition + * @param size + * @returns + */ + async findBy(condition: any, size?: number){ + const body = await this.objToBody(condition) + return this.find(body, size); + } + + /** + * 按字段值分页查找 + * @param condition + * @param page + * @param size + */ + async findPageBy(condition: any, page?: number, size?: number){ + const body = await this.objToBody(condition) + return this.findPage(body, page, size); + } + + /** + * 查询 + * @param body + */ + async find(body?: any, size?: number) { + if (!body) { + body = {}; + } + if(!body.size){ + body.size = size ? size : 10000; + } + return this.client + .search({ + index: this.index, + body, + }) + .then(res => { + return ( + res.hits.hits.map(e => { + e._source['id'] = e._id; + const _source: any = e._source; + ['_id', '_index', '_score', '_source'].forEach(key => { + delete e[key]; + }); + return { + ..._source, + ...e, + }; + }) || [] + ); + }); + } + + /** + * 分页查询 + * @param body + * @param page + * @param size + */ + async findPage(body?: any, page?: number, size?: number) { + if (!page) { + page = 1; + } + if (!body) { + body = {}; + } + if (!size && !body.size) { + size = 20; + body.size = size; + } + const total = await this.findCount(body); + body.from = (page - 1) * size; + return this.client.search({ index: this.index, body }).then(res => { + const result = + res.hits.hits.map(e => { + e._source['id'] = e._id; + const _source: any = e._source; + ['_id', '_index', '_score', '_source'].forEach(key => { + delete e[key]; + }); + return { + ..._source, + ...e, + }; + }) || []; + return { + list: result, + pagination: { + page, + size, + total, + }, + }; + }); + } + + /** + * 根据ID查询 + * @param id + * @returns + */ + async findById(id) { + return this.client + .get({ + index: this.index, + id, + }) + .then(res => { + res._source['id'] = res._id; + return res._source || undefined; + }) + .catch(e => { + return undefined; + }); + } + + /** + * 根据多个ID查询 + * @param ids + * @returns + */ + async findByIds(ids: string[]) { + return this.client + .mget({ index: this.index, body: { ids } }) + .then(res => { + const result = res.docs.map((e: any) => { + e._source.id = e._id; + return e._source || 'undefined'; + }); + return result.filter(e => { + return e !== 'undefined'; + }); + }) + .catch(e => { + return undefined; + }); + } + + /** + * 插入与更新 + * @param body + * @param refresh + * @param waitForActiveShards + * @returns + */ + async upsert( + body: any, + refresh?: boolean | 'wait_for', + waitForActiveShards?: WaitForActiveShards + ) { + if (refresh == undefined) { + refresh = true; + } + if (body.id) { + this.esDataChange('upsert', { + body, + refresh, + waitForActiveShards, + }); + const id = body.id; + delete body.id; + return this.client.index({ + id, + index: this.index, + wait_for_active_shards: waitForActiveShards, + refresh, + body, + }); + } else { + return this.client + .index({ + index: this.index, + wait_for_active_shards: waitForActiveShards, + refresh, + body, + }) + .then(res => { + this.esDataChange('upsert', { + body: { + ...body, + id: res._id, + }, + refresh, + waitForActiveShards, + }); + return res; + }); + } + } + + /** + * 批量插入更新 + * @param bodys + * @param type + * @param refresh + * @param waitForActiveShards + * @returns + */ + async batchIndex( + bodys: any[], + type: 'index' | 'create' | 'delete' | 'update', + refresh?: boolean | 'wait_for', + waitForActiveShards?: WaitForActiveShards + ) { + this.esDataChange('batchIndex', { + bodys, + type, + refresh, + waitForActiveShards, + }); + if (refresh == undefined) { + refresh = true; + } + const list = []; + for (const body of bodys) { + const typeO = {}; + typeO[type] = { _index: this.index, _id: body.id }; + if (body.id) { + delete body.id; + } + list.push(typeO); + if (type !== 'delete') { + if (type == 'update') { + list.push({ doc: body }); + } else { + list.push(body); + } + } + } + return this.client.bulk({ + wait_for_active_shards: waitForActiveShards, + index: this.index, + refresh, + body: list, + }); + } + + /** + * 删除索引 + * @param id + * @param refresh + * @param waitForActiveShards + * @returns + */ + async deleteById( + id, + refresh?: boolean | 'wait_for', + waitForActiveShards?: WaitForActiveShards + ) { + this.esDataChange('deleteById', { + id, + refresh, + waitForActiveShards, + }); + if (refresh == undefined) { + refresh = true; + } + try { + return this.client.delete({ + index: this.index, + refresh, + wait_for_active_shards: waitForActiveShards, + id, + }); + } catch {} + } + + /** + * 删除文档 + * @param ids + * @param refresh + * @param waitForActiveShards + * @returns + */ + async deleteByIds( + ids: string[], + refresh?: boolean, + waitForActiveShards?: WaitForActiveShards + ) { + this.esDataChange('deleteByIds', { + ids, + refresh, + waitForActiveShards, + }); + if (refresh == undefined) { + refresh = true; + } + const body = { + query: { + bool: { + must: [ + { + terms: { + _id: ids, + }, + }, + ], + }, + }, + }; + return this.client.deleteByQuery({ + index: this.index, + refresh, + wait_for_active_shards: waitForActiveShards, + body, + }); + } + + /** + * 根据条件批量删除 + * @param body + * @param refresh + * @param waitForActiveShards + * @returns + */ + async deleteByQuery( + body, + refresh?: boolean, + waitForActiveShards?: WaitForActiveShards + ) { + this.esDataChange('deleteByQuery', { + body, + refresh, + waitForActiveShards, + }); + if (refresh == undefined) { + refresh = true; + } + return this.client.deleteByQuery({ + index: this.index, + refresh, + wait_for_active_shards: waitForActiveShards, + body, + }); + } + + /** + * 更新索引 + * @param body + * @param refresh + * @param waitForActiveShards + * @returns + */ + async updateById( + body, + refresh?: boolean | 'wait_for', + waitForActiveShards?: WaitForActiveShards + ) { + this.esDataChange('updateById', { + body, + refresh, + waitForActiveShards, + }); + if (refresh == undefined) { + refresh = true; + } + const id = body.id; + delete body.id; + return this.client.update({ + wait_for_active_shards: waitForActiveShards, + index: this.index, + id: id, + refresh, + body: { + doc: body, + }, + }); + } + + /** + * 根据条件更新 + * @param body + * @param refresh + * @param waitForActiveShards + */ + async updateByQuery( + body, + refresh?: boolean, + waitForActiveShards?: WaitForActiveShards + ) { + this.esDataChange('updateByQuery', { + body, + refresh, + waitForActiveShards, + }); + if (refresh == undefined) { + refresh = true; + } + return this.client.updateByQuery({ + index: this.index, + refresh, + wait_for_active_shards: waitForActiveShards, + body, + }); + } + + /** + * 查询条数 + * @param body + */ + async findCount(body?: any) { + let _body = Object.assign({}, body || {}); + delete _body.from; + delete _body.size; + delete _body.sort; + return this.client + .count({ + index: this.index, + body: _body, + }) + .then(res => { + return res.count; + }) + .catch(e => { + return undefined; + }); + } + + /** + * 创建更新索引 + * @param config 配置 + */ + async createIndex( + properties: {}, + config: EsConfig = { + name: '', + replicas: 1, + shards: 8, + analyzers: [], + } + ) { + this.esDataChange('createIndex', { + properties, + config, + }); + const body = { + settings: { + number_of_shards: config.shards, + number_of_replicas: config.replicas, + analysis: { + analyzer: { + comma: { type: 'pattern', pattern: ',' }, + blank: { type: 'pattern', pattern: ' ' }, + }, + }, + mapping: { + nested_fields: { + limit: 100, + }, + }, + }, + mappings: { + properties: {}, + }, + }; + if (config.analyzers) { + for (const analyzer of config.analyzers) { + for (const key in analyzer) { + body.settings.analysis.analyzer[key] = analyzer[key]; + } + } + } + const param = { + index: this.index, + body, + }; + param.body = body; + param.body.mappings.properties = properties; + + this.client.indices.exists({ index: this.index }).then(async res => { + if (!res) { + await this.client.indices.create(param).then(res => { + if (res.acknowledged) { + console.info( + '\x1B[36m [cool:core] midwayjs cool elasticsearch ES索引创建成功: ' + + this.index + + ' \x1B[0m' + ); + } + }); + } else { + const updateParam = { + index: this.index, + body: param.body.mappings, + }; + await this.client.indices.putMapping(updateParam).then(res => { + if (res.acknowledged) { + console.info( + '\x1B[36m [cool:core] midwayjs cool elasticsearch ES索引更新成功: ' + + this.index + + ' \x1B[0m' + ); + } + }); + } + }); + } +} diff --git a/es/src/config/config.default.ts b/es/src/config/config.default.ts new file mode 100644 index 0000000..d8324ca --- /dev/null +++ b/es/src/config/config.default.ts @@ -0,0 +1,4 @@ +export const customKey = { + a: 1, + b: 'hello', +}; diff --git a/es/src/configuration.ts b/es/src/configuration.ts new file mode 100644 index 0000000..93d6842 --- /dev/null +++ b/es/src/configuration.ts @@ -0,0 +1,19 @@ +import { Configuration } from '@midwayjs/decorator'; +import * as DefaultConfig from './config/config.default'; +import { IMidwayContainer } from '@midwayjs/core'; +import { CoolElasticSearch } from './elasticsearch'; + +@Configuration({ + namespace: 'cool:es', + importConfigs: [ + { + default: DefaultConfig, + }, + ], +}) +export class CoolEsConfiguration { + async onReady(container: IMidwayContainer) { + await container.getAsync(CoolElasticSearch); + // TODO something + } +} diff --git a/es/src/decorator/elasticsearch.ts b/es/src/decorator/elasticsearch.ts new file mode 100644 index 0000000..7c89d0f --- /dev/null +++ b/es/src/decorator/elasticsearch.ts @@ -0,0 +1,41 @@ +import { + Scope, + ScopeEnum, + saveClassMetadata, + saveModule, +} from '@midwayjs/decorator'; + +export const COOL_ES_KEY = 'decorator:cool:es'; + +export interface EsConfig { + shards?: number; + name: string; + replicas?: number; + analyzers?: any[]; +} + +/** + * 索引 + * @param config + * @returns + */ +export function CoolEsIndex( + config: EsConfig | string = { + name: '', + replicas: 1, + shards: 8, + analyzers: [], + } +): ClassDecorator { + if (typeof config == 'string') { + config = { name: config, replicas: 1, shards: 8, analyzers: [] }; + } + return (target: any) => { + // 将装饰的类,绑定到该装饰器,用于后续能获取到 class + saveModule(COOL_ES_KEY, target); + // 保存一些元数据信息,任意你希望存的东西 + saveClassMetadata(COOL_ES_KEY, config, target); + // 指定 IoC 容器创建实例的作用域,这里注册为请求作用域,这样能取到 ctx + Scope(ScopeEnum.Singleton)(target); + }; +} diff --git a/es/src/elasticsearch.ts b/es/src/elasticsearch.ts new file mode 100644 index 0000000..0b925d2 --- /dev/null +++ b/es/src/elasticsearch.ts @@ -0,0 +1,154 @@ +import { + Provide, + getClassMetadata, + App, + Logger, + Inject, + Init, + Scope, + ScopeEnum, + Config, +} from '@midwayjs/decorator'; +import { COOL_ES_KEY, EsConfig } from './decorator/elasticsearch'; +import { listModule } from '@midwayjs/decorator'; +import { IMidwayApplication } from '@midwayjs/core'; +import { CoolCoreException, CoolEventManager } from '@cool-midway/core'; +import { ILogger } from '@midwayjs/logger'; +import { Client } from '@elastic/elasticsearch'; +import * as _ from 'lodash'; +import { CoolEsConfig, ICoolEs } from '.'; + +/** + * 搜索引擎 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolElasticSearch { + @App() + app: IMidwayApplication; + + @Logger() + coreLogger: ILogger; + + @Config('cool.es') + esConfig: CoolEsConfig; + + client: Client; + + @Inject('cool:coolEventManager') + coolEventManager: CoolEventManager; + + @Init() + async init() { + if (!this.esConfig?.nodes) { + throw new CoolCoreException('es.nodes config is require'); + } + if (this.esConfig.nodes.length == 1) { + this.client = new Client({ node: this.esConfig.nodes[0], ...this.esConfig.options }); + } else { + this.client = new Client({ nodes: this.esConfig.nodes, ...this.esConfig.options }); + } + this.client.ping({}, { requestTimeout: 30000 }).then(res => { + if (res) { + this.coolEventManager.emit('esReady', this.client); + this.scan(); + } + }); + } + + async scan() { + const modules = listModule(COOL_ES_KEY); + for (let module of modules) { + const cls: ICoolEs = await this.app + .getApplicationContext() + .getAsync(module); + const data = getClassMetadata(COOL_ES_KEY, module); + this.createIndex(cls, data); + } + } + + /** + * 数据更新事件 + * @param method + * @param data + */ + async esDataChange(method, data) { + //this.coolEventManager.emit('esDataChange', { method, data }); + } + + /** + * 创建索引 + * @param cls + * @param config + */ + async createIndex(cls, config: EsConfig) { + cls.index = config.name; + cls.client = this.client; + const body = { + settings: { + number_of_shards: config.shards, + number_of_replicas: config.replicas, + analysis: { + analyzer: { + comma: { type: 'pattern', pattern: ',' }, + blank: { type: 'pattern', pattern: ' ' }, + }, + }, + mapping: { + nested_fields: { + limit: 100, + }, + }, + }, + mappings: { + properties: {}, + }, + }; + if (config.analyzers) { + for (const analyzer of config.analyzers) { + for (const key in analyzer) { + body.settings.analysis.analyzer[key] = analyzer[key]; + } + } + } + const param = { + index: config.name, + body, + }; + param.body = body; + param.body.mappings.properties = cls.indexInfo(); + + this.esDataChange('createIndex', { + properties: param.body.mappings.properties, + config, + }); + + this.client.indices.exists({ index: config.name }).then(async res => { + if (!res) { + await this.client.indices.create(param).then(res => { + if (res.acknowledged) { + this.coreLogger.info( + '\x1B[36m [cool:core] midwayjs cool elasticsearch ES索引创建成功: ' + + config.name + + ' \x1B[0m' + ); + } + }); + } else { + const updateParam = { + index: config.name, + body: param.body.mappings, + }; + await this.client.indices.putMapping(updateParam).then(res => { + if (res.acknowledged) { + this.coreLogger.info( + '\x1B[36m [cool:core] midwayjs cool elasticsearch ES索引更新成功: ' + + config.name + + ' \x1B[0m' + ); + } + }); + } + }); + } +} diff --git a/es/src/index.ts b/es/src/index.ts new file mode 100644 index 0000000..6b42a6f --- /dev/null +++ b/es/src/index.ts @@ -0,0 +1,18 @@ +import { ClientOptions } from '@elastic/elasticsearch'; + +export { CoolEsConfiguration as Configuration } from './configuration'; + +export * from './elasticsearch'; + +export * from './decorator/elasticsearch'; + +export * from './base'; + +export interface ICoolEs { + indexInfo(): Object; +} + +export interface CoolEsConfig { + nodes: string[]; + options?: ClientOptions +} \ No newline at end of file diff --git a/es/src/package.json b/es/src/package.json new file mode 100644 index 0000000..7649088 --- /dev/null +++ b/es/src/package.json @@ -0,0 +1,41 @@ +{ + "name": "@cool-midway/es", + "version": "6.0.2", + "description": "cool-js.com elasticsearch", + "main": "index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [], + "author": "", + "files": [ + "**/*.js", + "**/*.d.ts" + ], + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "license": "MIT", + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^1.2.38", + "@midwayjs/core": "^3.0.0", + "@midwayjs/decorator": "^3.0.0", + "@midwayjs/mock": "^3.0.0", + "@types/jest": "^27.4.0", + "@types/node": "^16.11.22", + "cross-env": "^6.0.0", + "jest": "^27.5.1", + "mwts": "^1.0.5", + "ts-jest": "^27.1.3", + "typescript": "^4.0.0" + }, + "dependencies": { + "@elastic/elasticsearch": "^8.1.0" + } +} diff --git a/es/tsconfig.json b/es/tsconfig.json new file mode 100644 index 0000000..f01e1d2 --- /dev/null +++ b/es/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "inlineSourceMap":false, + "noImplicitThis": true, + "noUnusedLocals": true, + "stripInternal": true, + "skipLibCheck": true, + "noImplicitReturns": false, + "pretty": true, + "declaration": true, + "outDir": "dist" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/file/.editorconfig b/file/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/file/.editorconfig @@ -0,0 +1,11 @@ +# 🎨 editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/file/.eslintrc.json b/file/.eslintrc.json new file mode 100644 index 0000000..93e32bd --- /dev/null +++ b/file/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "extends": "./node_modules/mwts/", + "ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"], + "env": { + "jest": true + }, + "rules": { + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/ban-ts-comment": "off", + "node/no-extraneous-import": "off", + "no-empty": "off", + "node/no-extraneous-require": "off", + "eqeqeq": "off", + "node/no-unsupported-features/node-builtins": "off", + "@typescript-eslint/ban-types": "off", + "no-control-regex": "off", + "prefer-const": "off" + } +} diff --git a/file/.gitignore b/file/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/file/.gitignore @@ -0,0 +1,15 @@ +logs/ +npm-debug.log +yarn-error.log +node_modules/ +package-lock.json +yarn.lock +coverage/ +dist/ +.idea/ +run/ +.DS_Store +*.sw* +*.un~ +.tsbuildinfo +.tsbuildinfo.* diff --git a/file/.prettierrc.js b/file/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/file/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/file/LICENSE b/file/LICENSE new file mode 100644 index 0000000..5a739ee --- /dev/null +++ b/file/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013 - Now midwayjs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/file/index.d.ts b/file/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/file/index.d.ts @@ -0,0 +1,10 @@ +export * from './dist/index'; + +declare module '@midwayjs/core/dist/interface' { + interface MidwayConfig { + book?: PowerPartial<{ + a: number; + b: string; + }>; + } +} diff --git a/file/jest.config.js b/file/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/file/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], + setupFilesAfterEnv: ['./jest.setup.js'] +}; diff --git a/file/jest.setup.js b/file/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/file/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/file/package.json b/file/package.json new file mode 100644 index 0000000..ae2462f --- /dev/null +++ b/file/package.json @@ -0,0 +1,52 @@ +{ + "name": "@cool-midway/file", + "version": "6.0.2", + "description": "", + "main": "dist/index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [ + "cool", + "cool-admin", + "cooljs" + ], + "author": "COOL", + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts", + "index.d.ts" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^2.0.9", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@types/jest": "^29.2.5", + "@types/node": "^18.11.18", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "lodash": "^4.17.21", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typescript": "^4.9.4" + }, + "dependencies": { + "@midwayjs/upload": "^3.9.9", + "ali-oss": "^6.17.1", + "cos-nodejs-sdk-v5": "^2.11.19", + "download": "^8.0.0", + "qcloud-cos-sts": "^3.1.0", + "qiniu": "^7.8.0" + } +} diff --git a/file/src/config/config.default.ts b/file/src/config/config.default.ts new file mode 100644 index 0000000..9b0cf96 --- /dev/null +++ b/file/src/config/config.default.ts @@ -0,0 +1,16 @@ +import { CoolFileConfig } from './../interface'; +import { MODETYPE } from '../interface'; + +/** + * cool的配置 + */ +export default { + cool: { + file: { + // 上传模式 + mode: MODETYPE.LOCAL, + // 文件路径前缀 本地上传模式下 有效 + domain: 'http://127.0.0.1:8001', + } as CoolFileConfig, + }, +}; diff --git a/file/src/configuration.ts b/file/src/configuration.ts new file mode 100644 index 0000000..e4ef4ba --- /dev/null +++ b/file/src/configuration.ts @@ -0,0 +1,21 @@ +import { Configuration } from '@midwayjs/decorator'; +import * as DefaultConfig from './config/config.default'; +import * as upload from '@midwayjs/upload'; +import { IMidwayContainer } from '@midwayjs/core'; +import { CoolFile } from './file'; + +@Configuration({ + namespace: 'cool:file', + importConfigs: [ + { + default: DefaultConfig, + }, + ], + imports: [upload], +}) +export class CoolFileConfiguration { + async onReady(container: IMidwayContainer) { + await container.getAsync(CoolFile); + // TODO something + } +} diff --git a/file/src/file.ts b/file/src/file.ts new file mode 100644 index 0000000..720654d --- /dev/null +++ b/file/src/file.ts @@ -0,0 +1,476 @@ +import { + App, + Config, + Init, + Logger, + Provide, + Scope, + ScopeEnum, +} from '@midwayjs/decorator'; +import { Mode, CoolFileConfig, MODETYPE, CLOUDTYPE } from './interface'; +import { CoolCommException } from '@cool-midway/core'; +import * as moment from 'moment'; +import { v1 as uuid } from 'uuid'; +import * as path from 'path'; +import * as fs from 'fs'; +import { ILogger, IMidwayApplication } from '@midwayjs/core'; +import * as _ from 'lodash'; +import * as OSS from 'ali-oss'; +import * as crypto from 'crypto'; +import * as STS from 'qcloud-cos-sts'; +import * as download from 'download'; +import * as COS from 'cos-nodejs-sdk-v5'; +import * as QINIU from 'qiniu'; + +/** + * 文件上传 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolFile { + @Config('cool.file') + config: CoolFileConfig; + + @Logger() + coreLogger: ILogger; + + client: OSS & COS & QINIU.auth.digest.Mac; + + @App() + app: IMidwayApplication; + + @Init() + async init(config: CoolFileConfig) { + const filePath = path.join(this.app.getBaseDir(), '..', 'public'); + const uploadsPath = path.join(filePath, 'uploads'); + const tempPath = path.join(filePath, 'temp'); + if (!fs.existsSync(uploadsPath)) { + fs.mkdirSync(uploadsPath); + } + if (!fs.existsSync(tempPath)) { + fs.mkdirSync(tempPath); + } + if (config) { + this.config = config; + } + const { mode, oss, cos, qiniu } = this.config; + if (mode == MODETYPE.CLOUD) { + if (oss) { + const { accessKeyId, accessKeySecret, bucket, endpoint } = oss; + this.client = new OSS({ + region: endpoint.split('.')[0], + accessKeyId, + accessKeySecret, + bucket, + }); + } + if (cos) { + const { accessKeyId, accessKeySecret } = cos; + this.client = new COS({ + SecretId: accessKeyId, + SecretKey: accessKeySecret, + }); + } + if (qiniu) { + const { accessKeyId, accessKeySecret } = qiniu; + this.client = new QINIU.auth.digest.Mac(accessKeyId, accessKeySecret); + } + } + } + + /** + * 上传模式 + * @returns 上传模式 + */ + async getMode(): Promise { + const { mode, oss, cos, qiniu } = this.config; + if (mode == MODETYPE.LOCAL) { + return { + mode: MODETYPE.LOCAL, + type: MODETYPE.LOCAL, + }; + } + if (oss) { + return { + mode: MODETYPE.CLOUD, + type: CLOUDTYPE.OSS, + }; + } + if (cos) { + return { + mode: MODETYPE.CLOUD, + type: CLOUDTYPE.COS, + }; + } + if (qiniu) { + return { + mode: MODETYPE.CLOUD, + type: CLOUDTYPE.QINIU, + }; + } + } + + /** + * 获得原始操作对象 + * @returns + */ + getMetaFileObj(): OSS & COS & QINIU.auth.digest.Mac { + return this.client; + } + + /** + * 下载并上传 + * @param url + * @param fileName 文件名 + */ + async downAndUpload(url: string, fileName?: string) { + const { mode, oss, cos, qiniu, domain } = this.config; + let extend = ''; + if (url.includes('.')) { + const urlArr = url.split('.'); + extend = '.' + urlArr[urlArr.length - 1].split('?')[0]; + } + + const data = url.includes('http') + ? await download(url) + : fs.readFileSync(url); + + const isCloud = mode == MODETYPE.CLOUD; + // 创建文件夹 + const dirPath = path.join( + this.app.getBaseDir(), + '..', + `public/${isCloud ? 'temp' : 'uploads'}/${moment().format('YYYYMMDD')}` + ); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath); + } + const uuidStr = uuid(); + const name = `uploads/${moment().format('YYYYMMDD')}/${ + fileName ? fileName : uuidStr + extend + }`; + if (isCloud) { + if (oss) { + const ossClient: OSS = this.getMetaFileObj(); + return (await ossClient.put(name, data)).url; + } + if (cos) { + const cosClient: COS = this.getMetaFileObj(); + await cosClient.putObject({ + Bucket: cos.bucket, + Region: cos.region, + Key: name, + Body: data, + }); + return cos.publicDomain + '/' + name; + } + if (qiniu) { + let uploadToken = (await this.qiniu())['token']; + const formUploader = new QINIU.form_up.FormUploader(); + const putExtra = new QINIU.form_up.PutExtra(); + return new Promise((resolve, reject) => { + formUploader.put( + uploadToken, + name, + data, + putExtra, + (respErr, respBody, respInfo) => { + if (respErr) { + throw respErr; + } + if (respInfo.statusCode == 200) { + resolve(qiniu.publicDomain + '/' + name); + } + } + ); + }); + } + } else { + fs.writeFileSync( + `${dirPath}/${fileName ? fileName : uuidStr + extend}`, + data + ); + return `${domain}/public/${name}`; + } + } + + /** + * 指定Key(路径)上传 + * @param file + * @param key 路径一致会覆盖源文件 + */ + async uploadWithKey(file, key) { + const { mode, oss, cos, qiniu } = this.config; + const data = fs.readFileSync(file.data); + if (mode == MODETYPE.LOCAL) { + fs.writeFileSync(path.join(this.app.getBaseDir(), '..', key), data); + return this.config.domain + key; + } + if (mode == MODETYPE.CLOUD) { + if (oss) { + const ossClient: OSS = this.getMetaFileObj(); + return (await ossClient.put(key, data)).url; + } + if (cos) { + const cosClient: COS = this.getMetaFileObj(); + await cosClient.putObject({ + Bucket: cos.bucket, + Region: cos.region, + Key: key, + Body: data, + }); + return cos.publicDomain + '/' + key; + } + if (qiniu) { + let uploadToken = (await this.qiniu())['token']; + const formUploader = new QINIU.form_up.FormUploader(); + const putExtra = new QINIU.form_up.PutExtra(); + return new Promise((resolve, reject) => { + formUploader.put( + uploadToken, + key, + data, + putExtra, + (respErr, respBody, respInfo) => { + if (respErr) { + throw respErr; + } + if (respInfo.statusCode == 200) { + resolve(qiniu.publicDomain + '/' + key); + } + } + ); + }); + } + } + } + + /** + * 上传文件 + * @param ctx + * @param key 文件路径 + */ + async upload(ctx) { + const { mode, oss, cos, qiniu } = this.config; + if (mode == MODETYPE.LOCAL) { + return await this.local(ctx); + } + if (mode == MODETYPE.CLOUD) { + if (oss) { + return await this.oss(ctx); + } + if (cos) { + return await this.cos(ctx); + } + if (qiniu) { + return await this.qiniu(ctx); + } + } + } + + /** + * 七牛上传 + * @param ctx + * @returns + */ + private async qiniu(ctx?) { + const { + bucket, + publicDomain, + region, + uploadUrl = `https://upload-${region}.qiniup.com/`, + fileKey = 'file', + } = this.config.qiniu; + let options = { + scope: bucket, + }; + const putPolicy = new QINIU.rs.PutPolicy(options); + const uploadToken = putPolicy.uploadToken(this.client); + return new Promise((resolve, reject) => { + resolve({ + uploadUrl, + publicDomain, + token: uploadToken, + fileKey, + }); + }); + } + + /** + * OSS 文件上传 + * @param ctx + */ + private async oss(ctx) { + const { + accessKeyId, + accessKeySecret, + bucket, + endpoint, + expAfter = 300000, + maxSize = 200 * 1024 * 1024, + host, + } = this.config.oss; + const oss = { + bucket, + region: endpoint.split('.')[0], // 我的是 hangzhou + accessKeyId, + accessKeySecret, + expAfter, // 签名失效时间,毫秒 + maxSize, // 文件最大的 size + }; + const newHost = host ? host : `https://${bucket}.${endpoint}`; + const expireTime = new Date().getTime() + oss.expAfter; + const expiration = new Date(expireTime).toISOString(); + const policyString = JSON.stringify({ + expiration, + conditions: [ + ['content-length-range', 0, oss.maxSize], // 设置上传文件的大小限制,200mb + ], + }); + const policy = Buffer.from(policyString).toString('base64'); + + const signature = crypto + .createHmac('sha1', oss.accessKeySecret) + .update(policy) + .digest('base64'); + + return { + signature, + policy, + host: newHost, + OSSAccessKeyId: accessKeyId, + success_action_status: 200, + }; + } + + /** + * COS 文件上传 + * @param ctx + */ + private async cos(ctx) { + const { + accessKeyId, + accessKeySecret, + bucket, + region, + publicDomain, + durationSeconds = 1800, + allowPrefix = '_ALLOW_DIR_/*', + allowActions = [ + // 所有 action 请看文档 https://cloud.tencent.com/document/product/436/31923 + // 简单上传 + 'name/cos:PutObject', + 'name/cos:PostObject', + // 分片上传 + 'name/cos:InitiateMultipartUpload', + 'name/cos:ListMultipartUploads', + 'name/cos:ListParts', + 'name/cos:UploadPart', + 'name/cos:CompleteMultipartUpload', + ], + } = this.config.cos; + // 配置参数 + let config = { + secretId: accessKeyId, + secretKey: accessKeySecret, + durationSeconds, + bucket: bucket, + region: region, + // 允许操作(上传)的对象前缀,可以根据自己网站的用户登录态判断允许上传的目录,例子: user1/* 或者 * 或者a.jpg + // 请注意当使用 * 时,可能存在安全风险,详情请参阅:https://cloud.tencent.com/document/product/436/40265 + allowPrefix, + // 密钥的权限列表 + allowActions, + }; + + // 获取临时密钥 + let LongBucketName = config.bucket; + let ShortBucketName = LongBucketName.substring( + 0, + LongBucketName.lastIndexOf('-') + ); + let AppId = LongBucketName.substring(LongBucketName.lastIndexOf('-') + 1); + + let policy = { + version: '2.0', + statement: [ + { + action: config.allowActions, + effect: 'allow', + resource: [ + 'qcs::cos:' + + config.region + + ':uid/' + + AppId + + ':prefix//' + + AppId + + '/' + + ShortBucketName + + '/' + + config.allowPrefix, + ], + }, + ], + }; + + return new Promise((resolve, reject) => { + STS.getCredential( + { + secretId: config.secretId, + secretKey: config.secretKey, + durationSeconds: config.durationSeconds, + policy: policy, + }, + (err, tempKeys) => { + if (err) { + reject(err); + } + if (tempKeys) { + tempKeys.startTime = Math.round(Date.now() / 1000); + } + resolve({ + ...tempKeys, + url: publicDomain, + }); + } + ); + }); + } + + /** + * 本地上传 + * @param ctx + * @returns + */ + private async local(ctx) { + try { + const { key } = ctx.fields; + if (_.isEmpty(ctx.files)) { + throw new CoolCommException('上传文件为空'); + } + const file = ctx.files[0]; + const extension = file.filename.split('.').pop(); + const name = + moment().format('YYYYMMDD') + '/' + (key || `${uuid()}.${extension}`); + const target = path.join( + this.app.getBaseDir(), + '..', + `public/uploads/${name}` + ); + const dirPath = path.join( + this.app.getBaseDir(), + '..', + `public/uploads/${moment().format('YYYYMMDD')}` + ); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath); + } + const data = fs.readFileSync(file.data); + fs.writeFileSync(target, data); + return this.config.domain + '/public/uploads/' + name; + } catch (err) { + this.coreLogger.error(err); + throw new CoolCommException('上传失败'); + } + } +} diff --git a/file/src/index.ts b/file/src/index.ts new file mode 100644 index 0000000..5a58056 --- /dev/null +++ b/file/src/index.ts @@ -0,0 +1,5 @@ +export { CoolFileConfiguration as Configuration } from './configuration'; + +export * from './interface'; + +export * from './file'; diff --git a/file/src/interface.ts b/file/src/interface.ts new file mode 100644 index 0000000..db332ae --- /dev/null +++ b/file/src/interface.ts @@ -0,0 +1,105 @@ +// 模式 +export enum MODETYPE { + // 本地 + LOCAL = 'local', + // 云存储 + CLOUD = 'cloud', + // 其他 + OTHER = 'other', +} + +export enum CLOUDTYPE { + // 阿里云存储 + OSS = 'oss', + // 腾讯云存储 + COS = 'cos', + // 七牛云存储 + QINIU = 'qiniu', +} + +/** + * 上传模式 + */ +export interface Mode { + // 模式 + mode: MODETYPE; + // 类型 + type: string; +} + +/** + * 模块配置 + */ +export interface CoolFileConfig { + // 上传模式 + mode: MODETYPE; + // 阿里云oss 配置 + oss: OSSConfig; + // 腾讯云 cos配置 + cos: COSConfig; + // 七牛云 配置 + qiniu: QINIUConfig; + // 文件前缀 + domain: string; +} + +/** + * OSS 配置 + */ +export interface OSSConfig { + // 阿里云accessKeyId + accessKeyId: string; + // 阿里云accessKeySecret + accessKeySecret: string; + // 阿里云oss的bucket + bucket: string; + // 阿里云oss的endpoint + endpoint: string; + // 阿里云oss的timeout + timeout: string; + // 签名失效时间,毫秒 + expAfter?: number; + // 文件最大的 size + maxSize?: number; + // host + host?: string; +} + +/** + * COS 配置 + */ +export interface COSConfig { + // 腾讯云accessKeyId + accessKeyId: string; + // 腾讯云accessKeySecret + accessKeySecret: string; + // 腾讯云cos的bucket + bucket: string; + // 腾讯云cos的区域 + region: string; + // 腾讯云cos的公网访问地址 + publicDomain: string; + // 上传持续时间 + durationSeconds?: number; + // 允许操作(上传)的对象前缀 + allowPrefix?: string; + // 密钥的权限列表 + allowActions?: string[]; +} + +export interface QINIUConfig { + // 七牛云accessKeyId + accessKeyId: string; + // 七牛云accessKeySecret + accessKeySecret: string; + // 七牛云cos的bucket + bucket: string; + // 七牛云cos的区域 + region: string; + // 七牛云cos的公网访问地址 + publicDomain: string; + // 上传地址 + uploadUrl?: string; + // 上传fileKey + fileKey?: string; +} diff --git a/file/src/package.json b/file/src/package.json new file mode 100644 index 0000000..4f24725 --- /dev/null +++ b/file/src/package.json @@ -0,0 +1,52 @@ +{ + "name": "@cool-midway/file", + "version": "6.0.2", + "description": "", + "main": "index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [ + "cool", + "cool-admin", + "cooljs" + ], + "author": "COOL", + "files": [ + "**/*.js", + "**/*.d.ts", + "index.d.ts" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^2.0.9", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@types/jest": "^29.2.5", + "@types/node": "^18.11.18", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "lodash": "^4.17.21", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typescript": "^4.9.4" + }, + "dependencies": { + "@midwayjs/upload": "^3.9.1", + "ali-oss": "^6.17.1", + "cos-nodejs-sdk-v5": "^2.11.19", + "download": "^8.0.0", + "qcloud-cos-sts": "^3.1.0", + "qiniu": "^7.8.0" + } +} diff --git a/file/tsconfig.json b/file/tsconfig.json new file mode 100644 index 0000000..9bb4ab7 --- /dev/null +++ b/file/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "inlineSourceMap":false, + "noImplicitThis": true, + "noUnusedLocals": true, + "stripInternal": true, + "skipLibCheck": true, + "noImplicitReturns": false, + "pretty": true, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/iot/.editorconfig b/iot/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/iot/.editorconfig @@ -0,0 +1,11 @@ +# 🎨 editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/iot/.eslintrc.json b/iot/.eslintrc.json new file mode 100644 index 0000000..aad9755 --- /dev/null +++ b/iot/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "extends": "./node_modules/mwts/", + "ignorePatterns": [ + "node_modules", + "dist", + "test", + "jest.config.js", + "typings", + "public/**/**", + "view/**/**" + ], + "env": { + "jest": true + }, + "rules": { + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/ban-ts-comment": "off", + "node/no-extraneous-import": "off", + "no-empty": "off", + "node/no-extraneous-require": "off", + "eqeqeq": "off", + "node/no-unsupported-features/node-builtins": "off", + "@typescript-eslint/ban-types": "off", + "no-control-regex": "off", + "prefer-const": "off" + } +} \ No newline at end of file diff --git a/iot/.gitignore b/iot/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/iot/.gitignore @@ -0,0 +1,15 @@ +logs/ +npm-debug.log +yarn-error.log +node_modules/ +package-lock.json +yarn.lock +coverage/ +dist/ +.idea/ +run/ +.DS_Store +*.sw* +*.un~ +.tsbuildinfo +.tsbuildinfo.* diff --git a/iot/.prettierrc.js b/iot/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/iot/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/iot/README.md b/iot/README.md new file mode 100644 index 0000000..610c3db --- /dev/null +++ b/iot/README.md @@ -0,0 +1,3 @@ +# cool-admin + +https://cool-js.com diff --git a/iot/index.d.ts b/iot/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/iot/index.d.ts @@ -0,0 +1,10 @@ +export * from './dist/index'; + +declare module '@midwayjs/core/dist/interface' { + interface MidwayConfig { + book?: PowerPartial<{ + a: number; + b: string; + }>; + } +} diff --git a/iot/jest.config.js b/iot/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/iot/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], + setupFilesAfterEnv: ['./jest.setup.js'] +}; diff --git a/iot/jest.setup.js b/iot/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/iot/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/iot/package.json b/iot/package.json new file mode 100644 index 0000000..e000440 --- /dev/null +++ b/iot/package.json @@ -0,0 +1,52 @@ +{ + "name": "@cool-midway/iot", + "version": "6.0.0", + "description": "cool-js.com iot模块", + "main": "dist/index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [ + "cool", + "cool-admin", + "cooljs", + "cool-iot", + "iot" + ], + "author": "COOL", + "readme": "README.md", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts", + "index.d.ts" + ], + "devDependencies": { + "@cool-midway/core": "6.0.0", + "@midwayjs/cli": "^2.0.9", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@types/jest": "^29.2.5", + "@types/node": "^18.11.18", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typescript": "^4.9.4" + }, + "dependencies": { + "@cool-midway/mqemitter-redis": "^6.0.0", + "aedes": "^0.48.1", + "aedes-persistence-redis": "^9.0.2", + "aedes-server-factory": "^0.2.1" + } +} diff --git a/iot/src/config/config.default.ts b/iot/src/config/config.default.ts new file mode 100644 index 0000000..ee625da --- /dev/null +++ b/iot/src/config/config.default.ts @@ -0,0 +1,13 @@ +import { CoolIotConfig } from '../interface'; + +/** + * cool的配置 + */ +export default { + cool: { + iot: { + port: 1883, + wsPort: 8083, + } as CoolIotConfig, + }, +}; diff --git a/iot/src/configuration.ts b/iot/src/configuration.ts new file mode 100644 index 0000000..ecb17aa --- /dev/null +++ b/iot/src/configuration.ts @@ -0,0 +1,18 @@ +import { CoolMqttServe } from './mqtt'; +import { Configuration } from '@midwayjs/decorator'; +import * as DefaultConfig from './config/config.default'; +import { IMidwayContainer } from '@midwayjs/core'; + +@Configuration({ + namespace: 'cool:iot', + importConfigs: [ + { + default: DefaultConfig, + }, + ], +}) +export class CoolIotConfiguration { + async onReady(container: IMidwayContainer) { + (await container.getAsync(CoolMqttServe)).init(); + } +} diff --git a/iot/src/decorator/mqtt.ts b/iot/src/decorator/mqtt.ts new file mode 100644 index 0000000..8feb4f6 --- /dev/null +++ b/iot/src/decorator/mqtt.ts @@ -0,0 +1,42 @@ +import { + Scope, + ScopeEnum, + saveClassMetadata, + saveModule, + attachClassMetadata, +} from '@midwayjs/core'; + +export const COOL_MQTT_KEY = 'decorator:cool:cls:mqtt'; + +export function CoolMqtt(): ClassDecorator { + return (target: any) => { + // 将装饰的类,绑定到该装饰器,用于后续能获取到 class + saveModule(COOL_MQTT_KEY, target); + // 保存一些元数据信息,任意你希望存的东西 + saveClassMetadata(COOL_MQTT_KEY, {}, target); + // 指定 IoC 容器创建实例的作用域,这里注册为请求作用域,这样能取到 ctx + Scope(ScopeEnum.Singleton)(target); + }; +} + +export const COOL_MQTT_EVENT_KEY = 'decorator:cool:mqtt:event'; + +/** + * 事件 + * @param eventName + * @returns + */ +export function CoolMqttEvent(eventName?: string): MethodDecorator { + return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { + // 将装饰的类,绑定到该装饰器,用于后续能获取到 class + attachClassMetadata( + COOL_MQTT_EVENT_KEY, + { + eventName, + propertyKey, + descriptor, + }, + target + ); + }; +} diff --git a/iot/src/index.ts b/iot/src/index.ts new file mode 100644 index 0000000..e827cde --- /dev/null +++ b/iot/src/index.ts @@ -0,0 +1,7 @@ +export { CoolIotConfiguration as Configuration } from './configuration'; + +export * from './decorator/mqtt'; + +export * from './mqtt'; + +export * from './interface'; diff --git a/iot/src/interface.ts b/iot/src/interface.ts new file mode 100644 index 0000000..ca033f1 --- /dev/null +++ b/iot/src/interface.ts @@ -0,0 +1,34 @@ +import { AedesOptions } from 'aedes'; +import { PublishPacket } from 'packet'; + +/** + * MQTT配置 + */ +export interface CoolIotConfig { + /** MQTT服务端口 */ + port: number; + /** MQTT Websocket服务端口 */ + wsPort: number; + /** redis 配置 mqtt cluster下必须要配置 */ + redis?: { + /** host */ + host: string; + /** port */ + port: number; + /** password */ + password: string; + /** db */ + db: number; + }; + /** 发布消息配置 */ + publish?: PublishPacket; + /** 认证 */ + auth?: { + /** 用户 */ + username: string; + /** 密码 */ + password: string; + }; + /** 服务配置 */ + serve?: AedesOptions; +} diff --git a/iot/src/mqtt.ts b/iot/src/mqtt.ts new file mode 100644 index 0000000..7fade44 --- /dev/null +++ b/iot/src/mqtt.ts @@ -0,0 +1,163 @@ +import { CoolIotConfig } from './interface'; +import { + App, + Config, + getClassMetadata, + ILogger, + IMidwayApplication, + listModule, + Logger, + Provide, + Scope, + ScopeEnum, +} from '@midwayjs/core'; +import { COOL_MQTT_EVENT_KEY, COOL_MQTT_KEY } from './decorator/mqtt'; +import Aedes, { AedesOptions } from 'aedes'; +import { randomUUID } from 'crypto'; + +/** + * MQTT服务 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolMqttServe { + @Config('cool.iot') + coolIotConfig: CoolIotConfig; + + @Logger() + coreLogger: ILogger; + + serve: Aedes; + + @App() + app: IMidwayApplication; + + async init() { + await this.initServe(); + await this.handlerCls(); + await this.startServe(); + } + + /** + * 开启服务 + */ + async startServe() { + const { port, wsPort } = this.coolIotConfig; + const { createServer } = require('aedes-server-factory'); + const server = createServer(this.serve); + + const serverWs = createServer(this.serve, { ws: true }); + + server.listen(port, () => { + this.coreLogger.info( + `\x1B[36m [cool:iot] MQTT serve started port: ${port} \x1B[0m` + ); + }); + + serverWs.listen(wsPort, () => { + this.coreLogger.info( + `\x1B[36m [cool:iot] MQTT websocket serve started port: ${wsPort} \x1B[0m` + ); + }); + } + + /** + * 初始化服务 + */ + async initServe() { + const { redis } = this.coolIotConfig; + let option = {} as AedesOptions; + // cluster模式下必须配置redis + if (redis) { + const mqredis = require('@cool-midway/mqemitter-redis'); + const mq = mqredis(redis); + option.id = randomUUID(); + // redis cluster模式 + if (redis instanceof Array) { + option.persistence = require('aedes-persistence-redis')({ + cluster: redis, + maxSessionDelivery: 1000, // maximum offline messages deliverable on client CONNECT, default is 1000 + }); + } else { + option.persistence = require('aedes-persistence-redis')({ + ...redis, + maxSessionDelivery: 1000, // maximum offline messages deliverable on client CONNECT, default is 1000 + }); + } + option = { + id: randomUUID(), + mq, + }; + } + this.serve = require('aedes')(option); + + // 认证 + if (this.coolIotConfig.auth) { + const auth = this.coolIotConfig.auth; + this.serve.authenticate = function ( + client, + username, + password, + callback + ) { + callback( + null, + username === auth.username && password.toString() === auth.password + ); + }; + } + } + + /** + * 处理类 + */ + async handlerCls() { + const eventModules = listModule(COOL_MQTT_KEY); + for (const module of eventModules) { + this.handlerEvent(module); + } + } + + /** + * 处理事件 + * @param module + */ + async handlerEvent(module) { + const events = getClassMetadata(COOL_MQTT_EVENT_KEY, module); + for (const event of events) { + const method = event.eventName ? event.eventName : event.propertyKey; + this.serve.on(method, async (...args) => { + const moduleInstance = await this.app + .getApplicationContext() + .getAsync(module); + moduleInstance[event.propertyKey](...args); + }); + } + } + + /** + * 发送消息 + * @param topic 话题 + * @param message 消息 + * @param other 其他配置 + */ + async publish(topic, message, other?) { + this.serve.publish( + { + cmd: 'publish', + qos: 2, + dup: false, + topic, + payload: Buffer.from(message), + retain: false, + ...this.coolIotConfig.publish, + ...other, + }, + error => { + if (error) { + this.coreLogger.error('publish fail', error); + } + } + ); + } +} diff --git a/iot/src/package.json b/iot/src/package.json new file mode 100644 index 0000000..2530918 --- /dev/null +++ b/iot/src/package.json @@ -0,0 +1,52 @@ +{ + "name": "@cool-midway/iot", + "version": "6.0.0", + "description": "cool-js.com iot模块", + "main": "index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [ + "cool", + "cool-admin", + "cooljs", + "cool-iot", + "iot" + ], + "author": "COOL", + "readme": "README.md", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "files": [ + "**/*.js", + "**/*.d.ts", + "index.d.ts" + ], + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^2.0.9", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@types/jest": "^29.2.5", + "@types/node": "^18.11.18", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typescript": "^4.9.4" + }, + "dependencies": { + "aedes": "^0.48.1", + "aedes-persistence-redis": "^9.0.2", + "@cool-midway/mqemitter-redis": "^6.0.0", + "aedes-server-factory": "^0.2.1" + } +} diff --git a/iot/tsconfig.json b/iot/tsconfig.json new file mode 100644 index 0000000..f01e1d2 --- /dev/null +++ b/iot/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "inlineSourceMap":false, + "noImplicitThis": true, + "noUnusedLocals": true, + "stripInternal": true, + "skipLibCheck": true, + "noImplicitReturns": false, + "pretty": true, + "declaration": true, + "outDir": "dist" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/other/cache-manager-fs-hash/LICENSE b/other/cache-manager-fs-hash/LICENSE new file mode 100644 index 0000000..924498c --- /dev/null +++ b/other/cache-manager-fs-hash/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Roland Starke + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/other/cache-manager-fs-hash/README.md b/other/cache-manager-fs-hash/README.md new file mode 100644 index 0000000..b77338d --- /dev/null +++ b/other/cache-manager-fs-hash/README.md @@ -0,0 +1,81 @@ +# Node Cache Manager store for Filesystem + +[![Build Status](https://travis-ci.org/rolandstarke/node-cache-manager-fs-hash.svg?branch=master)](https://travis-ci.org/rolandstarke/node-cache-manager-fs-hash) +[![dependencies Status](https://david-dm.org/rolandstarke/node-cache-manager-fs-hash/status.svg)](https://david-dm.org/rolandstarke/node-cache-manager-fs-hash) +[![npm package](https://img.shields.io/npm/v/cache-manager-fs-hash.svg)](https://www.npmjs.com/package/cache-manager-fs-hash) +[![node](https://img.shields.io/node/v/cache-manager-fs-hash.svg)](https://nodejs.org) + +A Filesystem store for the [node-cache-manager](https://github.com/BryanDonovan/node-cache-manager) module + +## Installation + +```sh +npm install cache-manager-fs-hash --save +``` + +## Features + +* Saves anything that is `JSON.stringify`-able to disk +* Buffers are saved as well (if they reach a certain size they will be stored to separate files) +* Works well with the cluster module + +## Usage example + +Here is an example that demonstrates how to implement the Filesystem cache store. + +```javascript +const cacheManager = require('cache-manager'); +const fsStore = require('cache-manager-fs-hash'); + +const diskCache = cacheManager.caching({ + store: fsStore, + options: { + path: 'diskcache', //path for cached files + ttl: 60 * 60, //time to life in seconds + subdirs: true, //create subdirectories to reduce the + //files in a single dir (default: false) + zip: true, //zip files to save diskspace (default: false) + } +}); + + +(async () => { + + await diskCache.set('key', 'value'); + console.log(await diskCache.get('key')); //"value" + console.log(await diskCache.ttl('key')); //3600 seconds + await diskCache.del('key'); + console.log(await diskCache.get('key')); //undefined + + + console.log(await getUserCached(5)); //{id: 5, name: '...'} + console.log(await getUserCached(5)); //{id: 5, name: '...'} + + await diskCache.reset(); + + function getUserCached(userId) { + return diskCache.wrap(userId /* cache key */, function () { + return getUser(userId); + }); + } + + async function getUser(userId) { + return {id: userId, name: '...'}; + } + +})(); +``` + +## How it works + +The filename is determined by the md5 hash of the `key`. (The `key` is also saved in the file to detect hash collisions. In this case it will just return a cache miss). Writing is performed with .lock files so that multiple instances of the library (e.g. using the cluster module) do not interfere with one another. + +## Tests + +```sh +npm test +``` + +## License + +cache-manager-fs-hash is licensed under the MIT license. diff --git a/other/cache-manager-fs-hash/index.js b/other/cache-manager-fs-hash/index.js new file mode 100644 index 0000000..211e452 --- /dev/null +++ b/other/cache-manager-fs-hash/index.js @@ -0,0 +1 @@ +module.exports = require('./src'); \ No newline at end of file diff --git a/other/cache-manager-fs-hash/package.json b/other/cache-manager-fs-hash/package.json new file mode 100644 index 0000000..aac7a78 --- /dev/null +++ b/other/cache-manager-fs-hash/package.json @@ -0,0 +1,37 @@ +{ + "name": "@cool-midway/cache-manager-fs-hash", + "version": "6.0.0", + "main": "index.js", + "engines": { + "node": ">=8.0.0" + }, + "description": "file system store for node cache manager", + "author": "Roland Starke", + "license": "MIT", + "files": [ + "index.js", + "src/*" + ], + "keywords": [ + "cache-manager", + "storage", + "filesystem" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/rolandstarke/node-cache-manager-fs-hash.git" + }, + "bugs": { + "url": "https://github.com/rolandstarke/node-cache-manager-fs-hash/issues" + }, + "scripts": { + }, + "devDependencies": { + "cache-manager": "^3.2.1", + "mocha": "^7.1.1", + "rimraf": "^3.0.2" + }, + "dependencies": { + "lockfile": "^1.0.4" + } +} diff --git a/other/cache-manager-fs-hash/src/index.js b/other/cache-manager-fs-hash/src/index.js new file mode 100644 index 0000000..e500294 --- /dev/null +++ b/other/cache-manager-fs-hash/src/index.js @@ -0,0 +1,261 @@ +const fs = require('fs'); +const crypto = require('crypto'); +const path = require('path'); +const promisify = require('util').promisify; +const lockFile = require('lockfile'); +const jsonFileStore = require('./json-file-store'); +const wrapCallback = require('./wrap-callback'); + + +/** + * construction of the disk storage + * @param {object} [args] options of disk store + * @param {string} [args.path] path for cached files + * @param {number} [args.ttl] time to life in seconds + * @param {boolean} [args.zip] zip content to save diskspace + * @todo {number} [args.maxsize] max size in bytes on disk + * @param {boolean} [args.subdirs] create subdirectories + * @returns {DiskStore} + */ +exports.create = function (args) { + return new DiskStore(args && args.options ? args.options : args); +}; + +function DiskStore(options) { + options = options || {}; + + this.options = { + path: options.path || './cache', /* path for cached files */ + ttl: options.ttl, /* time before expiring in seconds */ + maxsize: options.maxsize || Infinity, /* max size in bytes on disk */ + subdirs: options.subdirs || false, + zip: options.zip || false, + lockFile: { //check lock at 0ms 50ms 100ms ... 400ms 1400ms 1450ms... up to 10 seconds, after that just asume the lock is staled + wait: 400, + pollPeriod: 50, + stale: 10 * 1000, + retries: 10, + retryWait: 600, + } + }; + + // check storage directory for existence (or create it) + if (!fs.existsSync(this.options.path)) { + fs.mkdirSync(this.options.path); + } +} + +/** + * save an entry in store + * @param {string} key + * @param {*} val + * @param {object} [options] + * @param {number} options.ttl time to life in seconds + * @param {function} [cb] + * @returns {Promise} + */ +DiskStore.prototype.set = wrapCallback(async function (key, val, options) { + key = key + ''; + const filePath = this._getFilePathByKey(key); + + const ttl = (options && (options.ttl >= 0)) ? +options.ttl : this.options.ttl; + const data = { + key: key, + val: val, + }; + if(ttl>0){ + data.expireTime = Date.now() + ttl * 1000; + } + + + if (this.options.subdirs) { + //check if subdir exists or create it + const dir = path.dirname(filePath); + await promisify(fs.access)(dir, fs.constants.W_OK).catch(function () { + return promisify(fs.mkdir)(dir).catch(err => { + if (err.code !== 'EEXIST') throw err; + }); + }); + } + + try { + await this._lock(filePath); + await jsonFileStore.write(filePath, data, this.options); + } catch (err) { + throw err; + } finally { + await this._unlock(filePath); + } +}); + + +DiskStore.prototype._readFile = async function (key) { + key = key + ''; + const filePath = this._getFilePathByKey(key); + + try { + const data = await jsonFileStore.read(filePath, this.options).catch(async (err) => { + if (err.code === 'ENOENT') { + throw err; + } + //maybe the file is currently written to, lets lock it and read again + try { + await this._lock(filePath); + return await jsonFileStore.read(filePath, this.options); + } catch (err2) { + throw err2; + } finally { + await this._unlock(filePath); + } + }); + if (data.expireTime <= Date.now()) { + //cache expired + this.del(key).catch(() => 0 /* ignore */); + return undefined; + } + if (data.key !== key) { + //hash collision + return undefined; + } + return data; + + } catch (err) { + //file does not exist lets return a cache miss + if (err.code === 'ENOENT') { + return undefined; + } else { + throw err; + } + } +}; + +/** + * get an entry from store + * @param {string} key + * @param {function} [cb] + * @returns {Promise} + */ +DiskStore.prototype.get = wrapCallback(async function (key) { + const data = await this._readFile(key); + if (data) { + return data.val; + } else { + return data; + } +}); + +/** + * get ttl in seconds for key in store + * @param {string} key + * @param {function} [cb] + * @returns {Promise} + */ +DiskStore.prototype.ttl = wrapCallback(async function (key) { + const data = await this._readFile(key); + if (data) { + return (data.expireTime - Date.now()) / 1000; + } else { + return 0; + } +}); + + +/** + * delete entry from cache + */ +DiskStore.prototype.del = wrapCallback(async function (key) { + const filePath = this._getFilePathByKey(key); + try { + if (this.options.subdirs) { + //check if the folder exists to fail faster + const dir = path.dirname(filePath); + await promisify(fs.access)(dir, fs.constants.W_OK); + } + + await this._lock(filePath); + await jsonFileStore.delete(filePath, this.options); + } catch (err) { + //ignore deleting non existing keys + if (err.code !== 'ENOENT') { + throw err; + } + } finally { + await this._unlock(filePath); + } +}); + + +/** + * cleanup cache on disk -> delete all files from the cache + */ +DiskStore.prototype.reset = wrapCallback(async function () { + const readdir = promisify(fs.readdir); + const stat = promisify(fs.stat); + const unlink = promisify(fs.unlink); + + return await deletePath(this.options.path, 2); + + async function deletePath(fileOrDir, maxDeep) { + if (maxDeep < 0) { + return; + } + const stats = await stat(fileOrDir); + if (stats.isDirectory()) { + const files = await readdir(fileOrDir); + for (let i = 0; i < files.length; i++) { + await deletePath(path.join(fileOrDir, files[i]), maxDeep - 1); + } + } else if (stats.isFile() && /[/\\]diskstore-[0-9a-fA-F/\\]+(\.json|-\d\.bin)/.test(fileOrDir)) { + //delete the file if it is a diskstore file + await unlink(fileOrDir); + } + } +}); + + +/** + * locks a file so other forks that want to use the same file have to wait + * @param {string} filePath + * @returns {Promise} + * @private + */ +DiskStore.prototype._lock = function (filePath) { + return promisify(lockFile.lock)( + filePath + '.lock', + JSON.parse(JSON.stringify(this.options.lockFile)) //the options are modified -> create a copy to prevent that + ); +}; + +/** + * unlocks a file path + * @type {Function} + * @param {string} filePath + * @returns {Promise} + * @private + */ +DiskStore.prototype._unlock = function (filePath) { + return promisify(lockFile.unlock)(filePath + '.lock'); +}; + +/** + * returns the location where the value should be stored + * @param {string} key + * @returns {string} + * @private + */ +DiskStore.prototype._getFilePathByKey = function (key) { + const hash = crypto.createHash('md5').update(key + '').digest('hex'); + if (this.options.subdirs) { + //create subdirs with the first 3 chars of the hash + return path.join( + this.options.path, + 'diskstore-' + hash.substr(0, 3), + hash.substr(3), + ); + } else { + return path.join( + this.options.path, + 'diskstore-' + hash + ); + } +}; \ No newline at end of file diff --git a/other/cache-manager-fs-hash/src/json-file-store.js b/other/cache-manager-fs-hash/src/json-file-store.js new file mode 100644 index 0000000..f655fd8 --- /dev/null +++ b/other/cache-manager-fs-hash/src/json-file-store.js @@ -0,0 +1,118 @@ +const promisify = require('util').promisify; +const fs = require('fs'); +const zlib = require('zlib'); + +exports.write = async function (path, data, options) { + const externalBuffers = []; + let dataString = JSON.stringify(data, function replacerFunction(k, value) { + //Buffers searilize to {data: [...], type: "Buffer"} + if (value && value.type === 'Buffer' && value.data && value.data.length >= 1024 /* only save bigger Buffers external, small ones can be inlined */) { + const buffer = Buffer.from(value.data); + externalBuffers.push({ + index: externalBuffers.length, + buffer: buffer, + }); + return { + type: 'ExternalBuffer', + index: externalBuffers.length - 1, + size: buffer.length, + }; + } else if (value === Infinity || value === -Infinity) { + return { type: 'Infinity', sign: Math.sign(value) }; + } else { + return value; + } + }); + + + let zipExtension = ''; + if (options.zip) { + zipExtension = '.gz'; + dataString = await promisify(zlib.deflate)(dataString); + } + //save main json file + await promisify(fs.writeFile)(path + '.json' + zipExtension, dataString, 'utf8'); + + //save external buffers + await Promise.all(externalBuffers.map(async function (externalBuffer) { + let buffer = externalBuffer.buffer; + if (options.zip) { + buffer = await promisify(zlib.deflate)(buffer); + } + await promisify(fs.writeFile)(path + '-' + externalBuffer.index + '.bin' + zipExtension, buffer, 'utf8'); + })); +}; + + +exports.read = async function (path, options) { + let zipExtension = ''; + if (options.zip) { + zipExtension = '.gz'; + } + + //read main json file + let dataString; + if (options.zip) { + const compressedData = await promisify(fs.readFile)(path + '.json' + zipExtension); + dataString = (await promisify(zlib.unzip)(compressedData)).toString(); + } else { + dataString = await promisify(fs.readFile)(path + '.json' + zipExtension, 'utf8'); + } + + + const externalBuffers = []; + const data = JSON.parse(dataString, function bufferReceiver(k, value) { + if (value && value.type === 'Buffer' && value.data) { + return Buffer.from(value.data); + } else if (value && value.type === 'ExternalBuffer' && typeof value.index === 'number' && typeof value.size === 'number') { + //JSON.parse is sync so we need to return a buffer sync, we will fill the buffer later + const buffer = Buffer.alloc(value.size); + externalBuffers.push({ + index: +value.index, + buffer: buffer, + }); + return buffer; + } else if (value && value.type === 'Infinity' && typeof value.sign === 'number') { + return Infinity * value.sign; + } else { + return value; + } + }); + + //read external buffers + await Promise.all(externalBuffers.map(async function (externalBuffer) { + + if (options.zip) { + const bufferCompressed = await promisify(fs.readFile)(path + '-' + +externalBuffer.index + '.bin' + zipExtension); + const buffer = await promisify(zlib.unzip)(bufferCompressed); + buffer.copy(externalBuffer.buffer); + } else { + const fd = await promisify(fs.open)(path + '-' + +externalBuffer.index + '.bin' + zipExtension, 'r'); + await promisify(fs.read)(fd, externalBuffer.buffer, 0, externalBuffer.buffer.length, 0); + await promisify(fs.close)(fd); + } + })); + return data; +}; + +exports.delete = async function (path, options) { + let zipExtension = ''; + if (options.zip) { + zipExtension = '.gz'; + } + + await promisify(fs.unlink)(path + '.json' + zipExtension); + + //delete binary files + try { + for (let i = 0; i < Infinity; i++) { + await promisify(fs.unlink)(path + '-' + i + '.bin' + zipExtension); + } + } catch (err) { + if (err.code === 'ENOENT') { + // every binary is deleted, we are done + } else { + throw err; + } + } +}; \ No newline at end of file diff --git a/other/cache-manager-fs-hash/src/wrap-callback.js b/other/cache-manager-fs-hash/src/wrap-callback.js new file mode 100644 index 0000000..aafc3ba --- /dev/null +++ b/other/cache-manager-fs-hash/src/wrap-callback.js @@ -0,0 +1,21 @@ +/** + * adds an callback param to the original function + * @param {function} fn + * @returns {function} + */ +module.exports = function wrapCallback(fn) { + return function (...args) { + let cb; + if (typeof args[args.length - 1] === 'function') { + cb = args.pop(); + } + + const promise = fn.apply(this, args); + + if (typeof cb === 'function') { + promise.then(value => setImmediate(cb, null, value), err => setImmediate(cb, err)); + } + + return promise; + }; +}; \ No newline at end of file diff --git a/other/mqemitter-redis/.github/dependabot.yml b/other/mqemitter-redis/.github/dependabot.yml new file mode 100644 index 0000000..4872c5a --- /dev/null +++ b/other/mqemitter-redis/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: +- package-ecosystem: npm + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/other/mqemitter-redis/.github/workflows/ci.yml b/other/mqemitter-redis/.github/workflows/ci.yml new file mode 100644 index 0000000..c9b1558 --- /dev/null +++ b/other/mqemitter-redis/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: ci + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x, 16.x, 18.x] + redis-tag: [5, 6] + + services: + redis: + image: redis:${{ matrix.redis-tag }} + ports: + - 6379:6379 + options: --entrypoint redis-server + + steps: + - uses: actions/checkout@v1 + + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + + - name: Install + run: | + npm install + + - name: Run tests + run: | + npm run test diff --git a/other/mqemitter-redis/.gitignore b/other/mqemitter-redis/.gitignore new file mode 100644 index 0000000..6c655ef --- /dev/null +++ b/other/mqemitter-redis/.gitignore @@ -0,0 +1,33 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Commenting this out is preferred by some people, see +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules + +# Users Environment Variables +.lock-wscript + +# ignore redis dump +dump.rdb + +package-lock.json diff --git a/other/mqemitter-redis/.travis.yml b/other/mqemitter-redis/.travis.yml new file mode 100644 index 0000000..73469a0 --- /dev/null +++ b/other/mqemitter-redis/.travis.yml @@ -0,0 +1,9 @@ +language: node_js +node_js: + - "10" + - "12" + - "13" + - "14" + - "15" +services: + - redis diff --git a/other/mqemitter-redis/LICENSE b/other/mqemitter-redis/LICENSE new file mode 100644 index 0000000..feb2ed0 --- /dev/null +++ b/other/mqemitter-redis/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014-2020 Matteo Collina + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/other/mqemitter-redis/README.md b/other/mqemitter-redis/README.md new file mode 100644 index 0000000..e4cd108 --- /dev/null +++ b/other/mqemitter-redis/README.md @@ -0,0 +1,76 @@ +mqemitter-redis  ![ci](https://github.com/mcollina/mqemitter/workflows/ci/badge.svg) +=============== + +Redis-powered [MQEmitter](http://github.com/mcollina/mqemitter). + +See [MQEmitter](http://github.com/mcollina/mqemitter) for the actual +API. + +[![js-standard-style](https://raw.githubusercontent.com/feross/standard/master/badge.png)](https://github.com/feross/standard) + + +Install +------- + +```bash +$ npm install mqemitter-redis --save +``` + +Example +------- + +```js +var redis = require('mqemitter-redis') +var mq = redis({ + port: 12345, + host: '12.34.56.78', + password: 'my secret', + db: 4 +}) +var msg = { + topic: 'hello world', + payload: 'or any other fields' +} + +mq.on('hello world', function (message, cb) { + // call callback when you are done + // do not pass any errors, the emitter cannot handle it. + cb() +}) + +// topic is mandatory +mq.emit(msg, function () { + // emitter will never return an error +}) +``` + +Connection String Example +------------------------- + +```js +var redis = require('mqemitter-redis') +var mq = redis({ + connectionString: 'redis://:authpassword@127.0.0.1:6380/4' +}) +``` + +## API + + +### MQEmitterRedis([opts]) + +Creates a new instance of mqemitter-redis. +It takes all the same options of [ioredis](http://npm.im/ioredis), +which is used internally to connect to Redis. + +This constructor creates two connections to Redis. + +Acknowledgements +---------------- + +Code ported from [Ascoltatori](http://github.com/mcollina/ascoltatori). + +License +------- + +MIT diff --git a/other/mqemitter-redis/mqemitter-redis.js b/other/mqemitter-redis/mqemitter-redis.js new file mode 100644 index 0000000..62f2222 --- /dev/null +++ b/other/mqemitter-redis/mqemitter-redis.js @@ -0,0 +1,250 @@ +"use strict"; + +const Redis = require("ioredis"); +const MQEmitter = require("mqemitter"); +const hyperid = require("hyperid")(); +const inherits = require("inherits"); +const LRU = require("lru-cache"); +const msgpack = require("msgpack-lite"); +const EE = require("events").EventEmitter; +const Pipeline = require("ioredis-auto-pipeline"); + +function MQEmitterRedis(opts) { + if (!(this instanceof MQEmitterRedis)) { + return new MQEmitterRedis(opts); + } + + opts = opts || {}; + + this._opts = opts; + + if (opts instanceof Array) { + this.subConn = new Redis.Cluster(opts); + this.pubConn = new Redis.Cluster(opts); + } else { + this.subConn = new Redis(opts.connectionString || opts); + this.pubConn = new Redis(opts.connectionString || opts); + } + + this._pipeline = Pipeline(this.pubConn); + + this._topics = {}; + + this._cache = new LRU({ + max: 10000, + ttl: 60 * 1000, // one minute + }); + + this.state = new EE(); + + const that = this; + + function onError(err) { + if (err && !that.closing) { + that.state.emit("error", err); + } + } + + this._onError = onError; + + function handler(sub, topic, payload) { + const packet = msgpack.decode(payload); + if (!that._cache.get(packet.id)) { + that._emit(packet.msg); + } + that._cache.set(packet.id, true); + } + + this.subConn.on("messageBuffer", function (topic, message) { + handler(topic, topic, message); + }); + + this.subConn.on("pmessageBuffer", function (sub, topic, message) { + handler(sub, topic, message); + }); + + this.subConn.on("connect", function () { + that.state.emit("subConnect"); + }); + + this.subConn.on("error", function (err) { + that._onError(err); + }); + + this.pubConn.on("connect", function () { + that.state.emit("pubConnect"); + }); + + this.pubConn.on("error", function (err) { + that._onError(err); + }); + + MQEmitter.call(this, opts); + + this._opts.regexWildcardOne = new RegExp( + this._opts.wildcardOne.replace(/([/,!\\^${}[\]().*+?|<>\-&])/g, "\\$&"), + "g" + ); + this._opts.regexWildcardSome = new RegExp( + (this._opts.matchEmptyLevels + ? this._opts.separator.replace(/([/,!\\^${}[\]().*+?|<>\-&])/g, "\\$&") + + "?" + : "") + + this._opts.wildcardSome.replace(/([/,!\\^${}[\]().*+?|<>\-&])/g, "\\$&"), + "g" + ); +} + +inherits(MQEmitterRedis, MQEmitter); +["emit", "on", "removeListener", "close"].forEach(function (name) { + MQEmitterRedis.prototype["_" + name] = MQEmitterRedis.prototype[name]; +}); + +MQEmitterRedis.prototype.close = function (cb) { + cb = cb || noop; + + if (this.closed || this.closing) { + return cb(); + } + + this.closing = true; + + let count = 2; + const that = this; + + function onEnd() { + if (--count === 0) { + that._close(cb); + } + } + + this.subConn.on("end", onEnd); + this.subConn.quit(); + + this.pubConn.on("end", onEnd); + this.pubConn.quit(); + + return this; +}; + +MQEmitterRedis.prototype._subTopic = function (topic) { + return topic + .replace(this._opts.regexWildcardOne, "*") + .replace(this._opts.regexWildcardSome, "*"); +}; + +MQEmitterRedis.prototype.on = function on(topic, cb, done) { + const subTopic = this._subTopic(topic); + const onFinish = function () { + if (done) { + setImmediate(done); + } + }; + + this._on(topic, cb); + + if (this._topics[subTopic]) { + this._topics[subTopic]++; + onFinish.call(this); + return this; + } + + this._topics[subTopic] = 1; + + if (this._containsWildcard(topic)) { + this.subConn.psubscribe(subTopic, onFinish.bind(this)); + } else { + this.subConn.subscribe(subTopic, onFinish.bind(this)); + } + + return this; +}; + +MQEmitterRedis.prototype.emit = function (msg, done) { + done = done || this._onError; + + if (this.closed) { + const err = new Error("mqemitter-redis is closed"); + return done(err); + } + + const packet = { + id: hyperid(), + msg, + }; + + this._pipeline + .publish(msg.topic, msgpack.encode(packet)) + .then(() => done()) + .catch(done); +}; + +MQEmitterRedis.prototype.removeListener = function (topic, cb, done) { + const subTopic = this._subTopic(topic); + const onFinish = function () { + if (done) { + setImmediate(done); + } + }; + + this._removeListener(topic, cb); + + if (--this._topics[subTopic] > 0) { + onFinish(); + return this; + } + + delete this._topics[subTopic]; + + if (this._containsWildcard(topic)) { + this.subConn.punsubscribe(subTopic, onFinish); + } else if (this._matcher.match(topic)) { + this.subConn.unsubscribe(subTopic, onFinish); + } + + return this; +}; + +MQEmitterRedis.prototype._containsWildcard = function (topic) { + return ( + topic.indexOf(this._opts.wildcardOne) >= 0 || + topic.indexOf(this._opts.wildcardSome) >= 0 + ); +}; + +function noop() {} + +module.exports = MQEmitterRedis; + +function MQEmitterRedisPrefix(pubSubPrefix, options) { + MQEmitterRedis.call(this, options); + this._pubSubPrefix = pubSubPrefix; + this._sym_proxiedCallback = Symbol("proxiedCallback"); +} +inherits(MQEmitterRedisPrefix, MQEmitterRedis); +MQEmitterRedisPrefix.prototype.on = function (topic, cb, done) { + const t = this._pubSubPrefix + topic; + cb[this._sym_proxiedCallback] = function (packet, cbcb) { + const t = packet.topic.slice(this._pubSubPrefix.length); + const p = { ...packet, topic: t }; + return cb.call(this, p, cbcb); + }.bind(this); + return MQEmitterRedis.prototype.on.call( + this, + t, + cb[this._sym_proxiedCallback], + done + ); +}; +MQEmitterRedisPrefix.prototype.removeListener = function (topic, func, done) { + const t = this._pubSubPrefix + topic; + const f = func[this._sym_proxiedCallback]; + return MQEmitterRedis.prototype.removeListener.call(this, t, f, done); +}; +MQEmitterRedisPrefix.prototype.emit = function (packet, done) { + const t = this._pubSubPrefix + packet.topic; + const p = { ...packet, topic: t }; + return MQEmitterRedis.prototype.emit.call(this, p, done); +}; + +module.exports.MQEmitterRedisPrefix = MQEmitterRedisPrefix; diff --git a/other/mqemitter-redis/package.json b/other/mqemitter-redis/package.json new file mode 100644 index 0000000..0e16666 --- /dev/null +++ b/other/mqemitter-redis/package.json @@ -0,0 +1,44 @@ +{ + "name": "@cool-midway/mqemitter-redis", + "version": "6.0.0", + "description": "Redis-based MQEmitter", + "main": "mqemitter-redis.js", + "types": "types/index.d.ts", + "dependencies": { + "hyperid": "^3.0.1", + "inherits": "^2.0.1", + "ioredis": "^5.0.4", + "ioredis-auto-pipeline": "^1.0.1", + "lru-cache": "^7.9.0", + "mqemitter": "^4.1.3", + "msgpack-lite": "^0.1.14" + }, + "devDependencies": { + "@types/ioredis": "^4.19.4", + "faucet": "^0.0.1", + "pre-commit": "^1.0.7", + "safe-buffer": "^5.1.2", + "standard": "^17.0.0", + "tape": "^5.0.1", + "tsd": "^0.20.0" + }, + "scripts": { + }, + "pre-commit": "test", + "repository": { + "type": "git", + "url": "https://github.com/mcollina/mqemitter-redis.git" + }, + "keywords": [ + "redis", + "mqemitter", + "emitter", + "pubsub", + "publish", + "subscribe", + "cool" + ], + "author": "COOL", + "license": "MIT", + "homepage": "https://cool-js.com" +} diff --git a/other/mqemitter-redis/types/index.d.ts b/other/mqemitter-redis/types/index.d.ts new file mode 100644 index 0000000..a6a5a6c --- /dev/null +++ b/other/mqemitter-redis/types/index.d.ts @@ -0,0 +1,37 @@ +import type { RedisOptions } from 'ioredis'; +import type { MQEmitter } from 'mqemitter'; + +export interface MQEmitterOptions { + concurrency?: number; + matchEmptyLevels?: boolean; + separator?: string; + wildcardOne?: string; + wildcardSome?: string; + connectionString?: string; +} + +export type Message = Record & { topic: string }; + +export interface MQEmitterRedis extends MQEmitter { + new (options?: MQEmitterOptions & RedisOptions): MQEmitterRedis; + current: number; + concurrent: number; + on( + topic: string, + listener: (message: Message, done: () => void) => void, + callback?: () => void + ): this; + emit(message: Message, callback?: (error?: Error) => void): void; + removeListener( + topic: string, + listener: (message: Message, done: () => void) => void, + callback?: () => void + ): void; + close(callback: () => void): void; +} + +declare function MQEmitterRedis( + options?: MQEmitterOptions & RedisOptions +): MQEmitterRedis; + +export default MQEmitterRedis; diff --git a/pay/.editorconfig b/pay/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/pay/.editorconfig @@ -0,0 +1,11 @@ +# 🎨 editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/pay/.eslintrc.json b/pay/.eslintrc.json new file mode 100644 index 0000000..aad9755 --- /dev/null +++ b/pay/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "extends": "./node_modules/mwts/", + "ignorePatterns": [ + "node_modules", + "dist", + "test", + "jest.config.js", + "typings", + "public/**/**", + "view/**/**" + ], + "env": { + "jest": true + }, + "rules": { + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/ban-ts-comment": "off", + "node/no-extraneous-import": "off", + "no-empty": "off", + "node/no-extraneous-require": "off", + "eqeqeq": "off", + "node/no-unsupported-features/node-builtins": "off", + "@typescript-eslint/ban-types": "off", + "no-control-regex": "off", + "prefer-const": "off" + } +} \ No newline at end of file diff --git a/pay/.gitignore b/pay/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/pay/.gitignore @@ -0,0 +1,15 @@ +logs/ +npm-debug.log +yarn-error.log +node_modules/ +package-lock.json +yarn.lock +coverage/ +dist/ +.idea/ +run/ +.DS_Store +*.sw* +*.un~ +.tsbuildinfo +.tsbuildinfo.* diff --git a/pay/.prettierrc.js b/pay/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/pay/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/pay/index.d.ts b/pay/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/pay/index.d.ts @@ -0,0 +1,10 @@ +export * from './dist/index'; + +declare module '@midwayjs/core/dist/interface' { + interface MidwayConfig { + book?: PowerPartial<{ + a: number; + b: string; + }>; + } +} diff --git a/pay/jest.config.js b/pay/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/pay/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], + setupFilesAfterEnv: ['./jest.setup.js'] +}; diff --git a/pay/jest.setup.js b/pay/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/pay/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/pay/package.json b/pay/package.json new file mode 100644 index 0000000..c1b9d32 --- /dev/null +++ b/pay/package.json @@ -0,0 +1,49 @@ +{ + "name": "@cool-midway/pay", + "version": "6.0.0", + "description": "cool-js.com 支付 微信 支付宝", + "main": "dist/index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [ + "cool", + "cool-admin", + "cooljs" + ], + "author": "COOL", + "readme": "README.md", + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts", + "index.d.ts" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^2.0.9", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@types/jest": "^29.2.5", + "@types/node": "^18.11.18", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typescript": "^4.9.4" + }, + "dependencies": { + "@4a/cid": "^0.1.0", + "alipay-sdk": "^3.2.0", + "wechatpay-node-v3": "^2.1.0" + } +} diff --git a/pay/src/ali.ts b/pay/src/ali.ts new file mode 100644 index 0000000..b46e992 --- /dev/null +++ b/pay/src/ali.ts @@ -0,0 +1,56 @@ +import { Config, Init, Provide, Scope, ScopeEnum } from '@midwayjs/decorator'; +import * as cid from '@4a/cid'; +import { CoolAliPayConfig } from './interface'; +import AlipaySdk from 'alipay-sdk'; + +/** + * 支付宝支付 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolAliPay { + pay: AlipaySdk; + + @Config('cool.pay.ali') + coolAlipay: CoolAliPayConfig; + + @Init() + async init() { + if (this.coolAlipay) this.pay = new AlipaySdk(this.coolAlipay); + } + + /** + * 获得支付宝支付SDK实例 + * @returns + */ + getInstance(): AlipaySdk { + return this.pay; + } + + /** + * 通知验签 + * @param postData {JSON} 服务端的消息内容 + * @param raw {Boolean} 是否使用 raw 内容而非 decode 内容验签 + */ + signVerify(postData: any, raw?: boolean) { + return this.pay.checkNotifySign(postData, raw); + } + + /** + * 创建订单 + * @param length 订单长度 + * @returns + */ + createOrderNum(length = 26) { + return cid(length); + } + + /** + * 动态配置支付参数 + * @param config 微信配置 + * @returns + */ + initPay(config: CoolAliPayConfig) { + return new AlipaySdk(config); + } +} diff --git a/pay/src/config/config.default.ts b/pay/src/config/config.default.ts new file mode 100644 index 0000000..d8324ca --- /dev/null +++ b/pay/src/config/config.default.ts @@ -0,0 +1,4 @@ +export const customKey = { + a: 1, + b: 'hello', +}; diff --git a/pay/src/configuration.ts b/pay/src/configuration.ts new file mode 100644 index 0000000..a77d2e5 --- /dev/null +++ b/pay/src/configuration.ts @@ -0,0 +1,21 @@ +import { Configuration } from '@midwayjs/decorator'; +import * as DefaultConfig from './config/config.default'; +import { IMidwayContainer } from '@midwayjs/core'; +import { CoolWxPay } from './wx'; +import { CoolAliPay } from './ali'; + +@Configuration({ + namespace: 'cool:pay', + importConfigs: [ + { + default: DefaultConfig, + }, + ], +}) +export class CoolPayConfiguration { + async onReady(container: IMidwayContainer) { + await container.getAsync(CoolWxPay); + await container.getAsync(CoolAliPay); + // TODO something + } +} diff --git a/pay/src/index.ts b/pay/src/index.ts new file mode 100644 index 0000000..c040890 --- /dev/null +++ b/pay/src/index.ts @@ -0,0 +1,7 @@ +export { CoolPayConfiguration as Configuration } from './configuration'; + +export * from './interface'; + +export * from './wx'; + +export * from './ali'; diff --git a/pay/src/interface.ts b/pay/src/interface.ts new file mode 100644 index 0000000..43eba55 --- /dev/null +++ b/pay/src/interface.ts @@ -0,0 +1,77 @@ +/** + * 微信支付配置 + */ +export interface CoolWxPayConfig { + // 直连商户申请的公众号或移动应用appid。 + appid: string; + // 商户号 + mchid: string; + // 可选参数 证书序列号 + serial_no?: string; + // 回调链接 + notify_url: string; + // 公钥 + publicKey: Buffer; + // 私钥 + privateKey: Buffer; + // 可选参数 认证类型,目前为WECHATPAY2-SHA256-RSA2048 + authType?: string; + // 可选参数 User-Agent + userAgent?: string; + // 可选参数 APIv3密钥 + key?: string; +} + +/** + * 支付宝支付配置 + */ +export interface CoolAliPayConfig { + // 支付回调地址 + notifyUrl: string; + /** 应用ID */ + appId: string; + /** + * 应用私钥字符串 + * RSA签名验签工具:https://docs.open.alipay.com/291/106097) + * 密钥格式一栏请选择 “PKCS1(非JAVA适用)” + */ + privateKey: string; + signType?: 'RSA2' | 'RSA'; + /** 支付宝公钥(需要对返回值做验签时候必填) */ + alipayPublicKey?: string; + /** 网关 */ + gateway?: string; + /** 网关超时时间(单位毫秒,默认 5s) */ + timeout?: number; + /** 是否把网关返回的下划线 key 转换为驼峰写法 */ + camelcase?: boolean; + /** 编码(只支持 utf-8) */ + charset?: 'utf-8'; + /** api版本 */ + version?: '1.0'; + urllib?: any; + /** 指定private key类型, 默认: PKCS1, PKCS8: PRIVATE KEY, PKCS1: RSA PRIVATE KEY */ + keyType?: 'PKCS1' | 'PKCS8'; + /** 应用公钥证书文件路径 */ + appCertPath?: string; + /** 应用公钥证书文件内容 */ + appCertContent?: string | Buffer; + /** 应用公钥证书sn */ + appCertSn?: string; + /** 支付宝根证书文件路径 */ + alipayRootCertPath?: string; + /** 支付宝根证书文件内容 */ + alipayRootCertContent?: string | Buffer; + /** 支付宝根证书sn */ + alipayRootCertSn?: string; + /** 支付宝公钥证书文件路径 */ + alipayPublicCertPath?: string; + /** 支付宝公钥证书文件内容 */ + alipayPublicCertContent?: string | Buffer; + /** 支付宝公钥证书sn */ + alipayCertSn?: string; + /** AES密钥,调用AES加解密相关接口时需要 */ + encryptKey?: string; + /** 服务器地址 */ + wsServiceUrl?: string; +} diff --git a/pay/src/package.json b/pay/src/package.json new file mode 100644 index 0000000..1854492 --- /dev/null +++ b/pay/src/package.json @@ -0,0 +1,49 @@ +{ + "name": "@cool-midway/pay", + "version": "6.0.0", + "description": "cool-js.com 支付 微信 支付宝", + "main": "index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [ + "cool", + "cool-admin", + "cooljs" + ], + "author": "COOL", + "readme": "README.md", + "files": [ + "**/*.js", + "**/*.d.ts", + "index.d.ts" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^2.0.9", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@types/jest": "^29.2.5", + "@types/node": "^18.11.18", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typescript": "^4.9.4" + }, + "dependencies": { + "@4a/cid": "^0.1.0", + "alipay-sdk": "^3.2.0", + "wechatpay-node-v3": "^2.1.0" + } +} diff --git a/pay/src/wx.ts b/pay/src/wx.ts new file mode 100644 index 0000000..09c67fc --- /dev/null +++ b/pay/src/wx.ts @@ -0,0 +1,68 @@ +import { CoolCommException } from '@cool-midway/core'; +import { Config, Init, Provide, Scope, ScopeEnum } from '@midwayjs/decorator'; +import * as cid from '@4a/cid'; +import { CoolWxPayConfig } from './interface'; +import WxPay = require('wechatpay-node-v3'); + +/** + * 微信支付 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolWxPay { + pay: WxPay; + + @Config('cool.pay.wx') + coolWxPay: CoolWxPayConfig; + + @Init() + async init() { + if (this.coolWxPay) this.pay = new WxPay(this.coolWxPay); + } + + /** + * 获得微信支付SDK实例 + * @returns + */ + getInstance(): WxPay { + return this.pay; + } + + /** + * 签名 + * @param params + * @returns + */ + async signVerify(ctx) { + if (!this.coolWxPay.key) { + throw new CoolCommException('未配置key(v3 API密钥)'); + } + const params = { + apiSecret: this.coolWxPay.key, // 如果在构造中传入了 key, 这里可以不传该值,否则需要传入该值 + body: ctx.request.body, // 请求体 body + signature: ctx.headers['wechatpay-signature'], + serial: ctx.headers['wechatpay-serial'], + nonce: ctx.headers['wechatpay-nonce'], + timestamp: ctx.headers['wechatpay-timestamp'], + }; + return await this.pay.verifySign(params); + } + + /** + * 创建订单 + * @param length 订单长度 + * @returns + */ + createOrderNum(length = 26) { + return cid(length); + } + + /** + * 动态配置支付参数 + * @param config 微信配置 + * @returns + */ + initPay(config: CoolWxPayConfig) { + return new WxPay(config); + } +} diff --git a/pay/tsconfig.json b/pay/tsconfig.json new file mode 100644 index 0000000..f01e1d2 --- /dev/null +++ b/pay/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "inlineSourceMap":false, + "noImplicitThis": true, + "noUnusedLocals": true, + "stripInternal": true, + "skipLibCheck": true, + "noImplicitReturns": false, + "pretty": true, + "declaration": true, + "outDir": "dist" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/rpc/.editorconfig b/rpc/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/rpc/.editorconfig @@ -0,0 +1,11 @@ +# 🎨 editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/rpc/.eslintrc.json b/rpc/.eslintrc.json new file mode 100644 index 0000000..0abd65a --- /dev/null +++ b/rpc/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "extends": "./node_modules/mwts/", + "ignorePatterns": [ + "node_modules", + "dist", + "test", + "jest.config.js", + "typings", + "public/**/**", + "view/**/**" + ], + "env": { + "jest": true + }, + "rules": { + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/ban-ts-comment": "off", + "node/no-extraneous-import": "off", + "@typescript-eslint/no-this-alias": "off", + "no-empty": "off", + "node/no-extraneous-require": "off", + "eqeqeq": "off", + "node/no-unsupported-features/node-builtins": "off", + "@typescript-eslint/ban-types": "off", + "no-control-regex": "off", + "prefer-const": "off" + } +} \ No newline at end of file diff --git a/rpc/.gitignore b/rpc/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/rpc/.gitignore @@ -0,0 +1,15 @@ +logs/ +npm-debug.log +yarn-error.log +node_modules/ +package-lock.json +yarn.lock +coverage/ +dist/ +.idea/ +run/ +.DS_Store +*.sw* +*.un~ +.tsbuildinfo +.tsbuildinfo.* diff --git a/rpc/.prettierrc.js b/rpc/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/rpc/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/rpc/index.d.ts b/rpc/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/rpc/index.d.ts @@ -0,0 +1,10 @@ +export * from './dist/index'; + +declare module '@midwayjs/core/dist/interface' { + interface MidwayConfig { + book?: PowerPartial<{ + a: number; + b: string; + }>; + } +} diff --git a/rpc/jest.config.js b/rpc/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/rpc/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], + setupFilesAfterEnv: ['./jest.setup.js'] +}; diff --git a/rpc/jest.setup.js b/rpc/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/rpc/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/rpc/package.json b/rpc/package.json new file mode 100644 index 0000000..6cb69cf --- /dev/null +++ b/rpc/package.json @@ -0,0 +1,47 @@ +{ + "name": "@cool-midway/rpc", + "version": "6.0.1", + "description": "cool-js.com rpc 微服务", + "main": "dist/index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [], + "author": "", + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts", + "index.d.ts" + ], + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "license": "MIT", + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^2.0.9", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@midwayjs/redis": "^3.9.0", + "@midwayjs/typeorm": "^3.9.5", + "@types/jest": "^29.2.5", + "@types/node": "^18.11.18", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "lodash": "^4.17.21", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typeorm": "^0.3.11", + "typescript": "^4.9.4" + }, + "dependencies": { + "ioredis": "4.28.5", + "moleculer": "^0.14.28" + } +} diff --git a/rpc/src/config/config.default.ts b/rpc/src/config/config.default.ts new file mode 100644 index 0000000..a30c406 --- /dev/null +++ b/rpc/src/config/config.default.ts @@ -0,0 +1,6 @@ +/** + * cool的配置 + */ +export default { + cool: {}, +}; diff --git a/rpc/src/configuration.ts b/rpc/src/configuration.ts new file mode 100644 index 0000000..16ae071 --- /dev/null +++ b/rpc/src/configuration.ts @@ -0,0 +1,26 @@ +import { Configuration } from '@midwayjs/decorator'; +import * as DefaultConfig from './config/config.default'; +import { IMidwayContainer } from '@midwayjs/core'; +import { CoolRpc } from './rpc'; +import { CoolRpcDecorator } from './decorator'; + +@Configuration({ + namespace: 'cool:rpc', + importConfigs: [ + { + default: DefaultConfig, + }, + ], +}) +export class CoolRpcConfiguration { + async onReady(container: IMidwayContainer) { + global['moleculer.transactions'] = {}; + (await container.getAsync(CoolRpc)).init(); + // 装饰器 + await container.getAsync(CoolRpcDecorator); + } + + async onStop(container: IMidwayContainer): Promise { + (await container.getAsync(CoolRpc)).stop(); + } +} diff --git a/rpc/src/decorator/event/event.ts b/rpc/src/decorator/event/event.ts new file mode 100644 index 0000000..526e11e --- /dev/null +++ b/rpc/src/decorator/event/event.ts @@ -0,0 +1,19 @@ +import { + Scope, + ScopeEnum, + saveClassMetadata, + saveModule, +} from '@midwayjs/decorator'; + +export const COOL_RPC_EVENT_KEY = 'decorator:cool:rpc:event'; + +export function CoolRpcEvent(): ClassDecorator { + return (target: any) => { + // 将装饰的类,绑定到该装饰器,用于后续能获取到 class + saveModule(COOL_RPC_EVENT_KEY, target); + // 保存一些元数据信息,任意你希望存的东西 + saveClassMetadata(COOL_RPC_EVENT_KEY, {}, target); + // 指定 IoC 容器创建实例的作用域 + Scope(ScopeEnum.Singleton)(target); + }; +} diff --git a/rpc/src/decorator/event/handler.ts b/rpc/src/decorator/event/handler.ts new file mode 100644 index 0000000..e5a8328 --- /dev/null +++ b/rpc/src/decorator/event/handler.ts @@ -0,0 +1,23 @@ +import { attachClassMetadata } from '@midwayjs/decorator'; + +export const COOL_RPC_EVENT_HANDLER_KEY = 'decorator:cool:rpc:event:handler'; + +/** + * 事件 + * @param eventName 事件名称 + * @returns + */ +export function CoolRpcEventHandler(eventName?: string): MethodDecorator { + return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { + // 将装饰的类,绑定到该装饰器,用于后续能获取到 class + attachClassMetadata( + COOL_RPC_EVENT_HANDLER_KEY, + { + propertyKey, + descriptor, + eventName, + }, + target + ); + }; +} diff --git a/rpc/src/decorator/index.ts b/rpc/src/decorator/index.ts new file mode 100644 index 0000000..3299815 --- /dev/null +++ b/rpc/src/decorator/index.ts @@ -0,0 +1,101 @@ +import { CoolCommException } from '@cool-midway/core'; +import { + Provide, + Scope, + ScopeEnum, + JoinPoint, + Init, + MidwayDecoratorService, + Inject, +} from '@midwayjs/core'; +import { TypeORMDataSourceManager } from '@midwayjs/typeorm'; +import { COOL_RPC_TRANSACTION, TransactionOptions } from './transaction'; +import { v1 as uuid } from 'uuid'; + +/** + * 装饰器 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolRpcDecorator { + @Inject() + decoratorService: MidwayDecoratorService; + + @Inject() + typeORMDataSourceManager: TypeORMDataSourceManager; + + @Init() + async init() { + // 事务 + await this.transaction(); + } + + /** + * 事务 + */ + async transaction() { + this.decoratorService.registerMethodHandler( + COOL_RPC_TRANSACTION, + options => { + return { + around: async (joinPoint: JoinPoint) => { + const option: TransactionOptions = options.metadata; + let isCaller = false; + let rpcTransactionId; + if (joinPoint.args[0]) { + isCaller = false; + rpcTransactionId = joinPoint.args[0].rpcTransactionId; + } + // 如果没有事务ID,手动创建 + if (!rpcTransactionId) { + isCaller = true; + rpcTransactionId = uuid(); + } + + let data; + const dataSource = this.typeORMDataSourceManager.getDataSource( + option?.connectionName || 'default' + ); + const queryRunner = dataSource.createQueryRunner(); + // 使用我们的新queryRunner建立真正的数据库连 + await queryRunner.connect(); + if (option && option.isolation) { + await queryRunner.startTransaction(option.isolation); + } else { + await queryRunner.startTransaction(); + } + + try { + global['moleculer.transactions'][rpcTransactionId] = queryRunner; + // 半小时后清除 + setTimeout(() => { + global['moleculer.transactions'][rpcTransactionId].release(); + delete global['moleculer.transactions'][rpcTransactionId]; + }, 1800 * 1000); + joinPoint.args.push(rpcTransactionId); + joinPoint.args.push(queryRunner); + data = await joinPoint.proceed(...joinPoint.args); + if (isCaller) { + global['moleculer:broker'].broadcast('moleculer.transaction', { + rpcTransactionId, + commit: true, + }); + } + //await queryRunner.commitTransaction(); + } catch (error) { + //await queryRunner.rollbackTransaction(); + if (isCaller) { + global['moleculer:broker'].broadcast('moleculer.transaction', { + rpcTransactionId, + commit: false, + }); + } + throw new CoolCommException(error.message); + } + return data; + }, + }; + } + ); + } +} diff --git a/rpc/src/decorator/rpc.ts b/rpc/src/decorator/rpc.ts new file mode 100644 index 0000000..8e91b4b --- /dev/null +++ b/rpc/src/decorator/rpc.ts @@ -0,0 +1,84 @@ +import { + Scope, + ScopeEnum, + saveClassMetadata, + saveModule, +} from '@midwayjs/decorator'; + +export const MOLECYLER_KEY = 'decorator:cool:rpc'; + +export type MethodTypes = + | 'add' + | 'delete' + | 'update' + | 'page' + | 'info' + | 'list'; + +// 字段匹配 +export interface FieldEq { + // 字段 + column: string; + // 请求参数 + requestParam: string; +} + +// 关联查询 +export interface LeftJoinOp { + // 实体 + entity: any; + // 别名 + alias: string; + // 关联条件 + condition: string; +} + +// Crud配置 +export interface CurdOption { + // 路由前缀,不配置默认是按Controller下的文件夹路径 + prefix?: string; + // curd api接口 + method: MethodTypes[]; + // 分页查询配置 + pageQueryOp?: QueryOp; + // 非分页查询配置 + listQueryOp?: QueryOp; + // 插入参数 + insertParam?: Function; + // info 忽略返回属性 + infoIgnoreProperty?: string[]; + // 实体 + entity: { entityKey?: any; connectionName?: string } | any; +} + +// 查询配置 +export interface QueryOp { + // 需要模糊查询的字段 + keyWordLikeFields?: string[]; + // 查询条件 + where?: Function; + // 查询字段 + select?: string[]; + // 字段相等 + fieldEq?: string[] | FieldEq[]; + // 添加排序条件 + addOrderBy?: {}; + // 关联配置 + leftJoin?: LeftJoinOp[]; +} + +/** + * moleculer 微服务配置 + * @param option + * @returns + */ +export function CoolRpcService(option?: CurdOption): ClassDecorator { + return (target: any) => { + // 将装饰的类,绑定到该装饰器,用于后续能获取到 class + saveModule(MOLECYLER_KEY, target); + // 保存一些元数据信息,任意你希望存的东西 + saveClassMetadata(MOLECYLER_KEY, option, target); + // 指定 IoC 容器创建实例的作用域,这里注册为请求作用域,这样能取到 ctx + Scope(ScopeEnum.Request)(target); + }; +} diff --git a/rpc/src/decorator/transaction.ts b/rpc/src/decorator/transaction.ts new file mode 100644 index 0000000..93cc5ca --- /dev/null +++ b/rpc/src/decorator/transaction.ts @@ -0,0 +1,22 @@ +import * as _ from 'lodash'; +import { createCustomMethodDecorator } from '@midwayjs/core'; + +type IsolationLevel = + | 'READ UNCOMMITTED' + | 'READ COMMITTED' + | 'REPEATABLE READ' + | 'SERIALIZABLE'; + +export interface TransactionOptions { + connectionName?: string; + isolation?: IsolationLevel; +} + +// 装饰器内部的唯一 id +export const COOL_RPC_TRANSACTION = 'decorator:cool_rpc_transaction'; + +export function CoolRpcTransaction( + option?: TransactionOptions +): MethodDecorator { + return createCustomMethodDecorator(COOL_RPC_TRANSACTION, option); +} diff --git a/rpc/src/index.ts b/rpc/src/index.ts new file mode 100644 index 0000000..4d68d39 --- /dev/null +++ b/rpc/src/index.ts @@ -0,0 +1,25 @@ +export { CoolRpcConfiguration as Configuration } from './configuration'; + +export * from './test'; +export * from './rpc'; +export * from './decorator/rpc'; +export * from './decorator/event/event'; +export * from './decorator/event/handler'; +export * from './service/base'; +export * from './decorator/transaction'; +export * from './transaction/event'; +export * from './decorator/index'; + +export interface CoolRpcConfig { + // 服务名称 + name: string; + // redis + redis: RedisConfig & RedisConfig[] & unknown; +} + +export interface RedisConfig { + host: string; + password: string; + port: number; + db: number; +} diff --git a/rpc/src/package.json b/rpc/src/package.json new file mode 100644 index 0000000..73c721e --- /dev/null +++ b/rpc/src/package.json @@ -0,0 +1,47 @@ +{ + "name": "@cool-midway/rpc", + "version": "6.0.1", + "description": "cool-js.com rpc 微服务", + "main": "index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [], + "author": "", + "files": [ + "**/*.js", + "**/*.d.ts", + "index.d.ts" + ], + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "license": "MIT", + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^2.0.9", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@midwayjs/redis": "^3.9.0", + "@midwayjs/typeorm": "^3.9.5", + "@types/jest": "^29.2.5", + "@types/node": "^18.11.18", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "lodash": "^4.17.21", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typeorm": "^0.3.11", + "typescript": "^4.9.4" + }, + "dependencies": { + "ioredis": "4.28.5", + "moleculer": "^0.14.28" + } +} diff --git a/rpc/src/rpc.ts b/rpc/src/rpc.ts new file mode 100644 index 0000000..1d6c1de --- /dev/null +++ b/rpc/src/rpc.ts @@ -0,0 +1,275 @@ +import { ILogger, IMidwayApplication, Inject } from '@midwayjs/core'; +import { + App, + Config, + getClassMetadata, + listModule, + Logger, + Provide, + Scope, + ScopeEnum, +} from '@midwayjs/decorator'; +import { ServiceBroker } from 'moleculer'; +import { CoolRpcConfig } from '.'; +import { CoolCoreException, CoolValidateException } from '@cool-midway/core'; +import { v1 as uuid } from 'uuid'; +import { BaseRpcService } from './service/base'; +import { CurdOption, MOLECYLER_KEY } from './decorator/rpc'; +import { COOL_RPC_EVENT_KEY } from './decorator/event/event'; +import { COOL_RPC_EVENT_HANDLER_KEY } from './decorator/event/handler'; +import * as _ from 'lodash'; +import { TypeORMDataSourceManager } from '@midwayjs/typeorm'; +import { camelCase } from '@midwayjs/core/dist/util/camelCase'; +// import { AgentService } from '@moleculer/lab'; + +/** + * 微服务 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolRpc { + broker: ServiceBroker; + + @Inject() + typeORMDataSourceManager: TypeORMDataSourceManager; + + @Logger() + coreLogger: ILogger; + + @Config('cool.rpc') + rpcConfig: CoolRpcConfig; + + @Config('cool') + coolConfig; + + @App() + app: IMidwayApplication; + + cruds; + + async init() { + if (!this.rpcConfig?.name) { + throw new CoolCoreException( + 'cool.rpc.name config is require and every service name must be unique' + ); + } + + let redisConfig; + + if (!this.rpcConfig?.redis && !this.coolConfig?.redis) { + throw new CoolCoreException('cool.rpc.redis or cool.redis is require'); + } + + redisConfig = this.rpcConfig?.redis + ? this.rpcConfig?.redis + : this.coolConfig?.redis; + + const transporter = { + type: 'Redis', + options: {}, + }; + if (redisConfig instanceof Array) { + transporter.options = { + cluster: { + nodes: redisConfig, + }, + }; + } else { + transporter.options = redisConfig; + } + + this.broker = new ServiceBroker({ + nodeID: `${this.rpcConfig.name}-${uuid()}`, + transporter, + // metrics: { + // enabled: true, + // reporter: 'Laboratory', + // }, + // tracing: { + // enabled: true, + // exporter: 'Laboratory', + // }, + ...this.rpcConfig, + }); + + // this.broker.createService({ + // name: this.rpcConfig.name, + // mixins: [], + // // settings: { + // // name: 'test', + // // port: 3210, + // // token: '123123', + // // apiKey: '92C18ZR-ERM45EG-HT8GQGQ-4MHCXAT', + // // }, + // }); + + global['moleculer:broker'] = this.broker; + + await this.initService(); + await this.createService(); + } + + /** + * 获得事件 + * @returns + */ + async getEvents() { + const allEvents = {}; + const modules = listModule(COOL_RPC_EVENT_KEY); + for (const module of modules) { + const moduleInstance = await this.app + .getApplicationContext() + .getAsync(module); + moduleInstance['broker'] = this.broker; + const events = getClassMetadata(COOL_RPC_EVENT_HANDLER_KEY, module); + for (const event of events) { + allEvents[event.eventName ? event.eventName : event.propertyKey] = { + handler(ctx) { + moduleInstance[event.propertyKey](ctx.params); + }, + }; + } + } + return allEvents; + } + + /** + * 创建服务 + */ + async createService() { + const _this = this; + this.broker.createService({ + name: this.rpcConfig.name, + events: await this.getEvents(), + actions: { + async call(ctx) { + const { service, method, params } = ctx.params; + const targetName = _.upperFirst(service); + const target = _.find(_this.cruds, { name: targetName }); + if (!target) { + throw new CoolValidateException('找不到服务'); + } + const curdOption: CurdOption = getClassMetadata( + MOLECYLER_KEY, + target + ); + + const cls = await _this.app + .getApplicationContext() + .getAsync(camelCase(service)); + const serviceInstance: BaseRpcService = new target(); + Object.assign(serviceInstance, cls); + serviceInstance.setModel(_this.getModel(curdOption)); + serviceInstance.setApp(_this.app); + serviceInstance.init(); + + // 如果是通用crud方法 注入参数 + if ( + ['add', 'delete', 'update', 'page', 'info', 'list'].includes(method) + ) { + if (!curdOption.method.includes(method)) { + throw new CoolValidateException('方法不存在'); + } + } + return serviceInstance[method](params); + }, + }, + }); + this.broker.start(); + } + + /** + * 初始化service,设置entity + */ + async initService() { + // 获得所有的service + this.cruds = listModule(MOLECYLER_KEY); + for (const crud of this.cruds) { + const curdOption: CurdOption = getClassMetadata(MOLECYLER_KEY, crud); + const serviceInstance: BaseRpcService = await this.app + .getApplicationContext() + .getAsync(crud); + serviceInstance.setModel(this.getModel(curdOption)); + serviceInstance.setCurdOption(curdOption); + } + } + + /** + * 获得Model + * @param curdOption + */ + getModel(curdOption) { + // 获得到model + let entityModel; + const { entity } = curdOption || {}; + if (entity) { + const dataSourceName = + this.typeORMDataSourceManager.getDataSourceNameByModel(entity); + entityModel = this.typeORMDataSourceManager + .getDataSource(dataSourceName) + .getRepository(entity); + } + return entityModel; + } + + /** + * 调用服务 + * @param name 服务名称 + * @param controller 接口服务 + * @param method 方法 + * @param params 参数 + * @returns + */ + async call(name: string, service: string, method: string, params?: {}) { + return this.broker.call(`${name}.call`, { service, method, params }); + } + + /** + * 发送事件 + * @param name 事件名称 + * @param params 事件参数 + * @param node 节点名称 + */ + async event(name: string, params: any, node?: string | string[]) { + this.broker.emit(name, params); + } + + /** + * 发送广播事件 + * @param name + * @param params + * @param node 节点名称 + */ + async broadcastEvent(name: string, params: any, node?: string | string[]) { + this.broker.broadcast(name, params); + } + + /** + * 发送本地广播事件 + * @param name + * @param params + * @param node 节点名称 + */ + async broadcastLocalEvent( + name: string, + params: any, + node?: string | string[] + ) { + this.broker.broadcastLocal(name, params); + } + + /** + * 获得原始的broker对象 + * @returns + */ + getBroker() { + return this.broker; + } + + /** + * 停止 + */ + stop() { + this.broker.stop(); + } +} diff --git a/rpc/src/service/base.ts b/rpc/src/service/base.ts new file mode 100644 index 0000000..8d0abf3 --- /dev/null +++ b/rpc/src/service/base.ts @@ -0,0 +1,406 @@ +import { Config, Init, Provide, App } from '@midwayjs/decorator'; +import { Brackets } from 'typeorm'; +import * as _ from 'lodash'; +import { CoolValidateException, ERRINFO } from '@cool-midway/core'; +import { QueryOp } from '../decorator/rpc'; +import { IMidwayApplication, Inject } from '@midwayjs/core'; +import * as SqlString from 'sqlstring'; +import { TypeORMDataSourceManager } from '@midwayjs/typeorm'; + +/** + * 服务基类 + */ +@Provide() +export abstract class BaseRpcService { + // 分页配置 + @Config('cool.page') + private conf; + + // 模型 + protected entity; + + protected sqlParams; + + protected curdOption; + + @Inject() + typeORMDataSourceManager: TypeORMDataSourceManager; + + // 设置模型 + setModel(entity: any) { + this.entity = entity; + } + + setCurdOption(curdOption) { + this.curdOption = curdOption; + } + + @App() + app: IMidwayApplication; + + setApp(app) { + this.app = app; + } + + // 初始化 + @Init() + init() { + this.sqlParams = []; + } + + /** + * 检查排序 + * @param sort 排序 + * @returns + */ + private checkSort(sort) { + if (!['desc', 'asc'].includes(sort.toLowerCase())) { + throw new CoolValidateException('sort 非法传参~'); + } + return sort; + } + + /** + * 获得单个ID + * @param params 参数 + */ + async info(params: any): Promise { + const { id } = params; + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + if (!id) { + throw new CoolValidateException(ERRINFO.NOID); + } + const info = await this.entity.findOne({ id }); + if (info && this.curdOption.infoIgnoreProperty) { + for (const property of this.curdOption.infoIgnoreProperty) { + delete info[property]; + } + } + return info; + } + + /** + * 执行SQL并获得分页数据 + * @param sql 执行的sql语句 + * @param query 分页查询条件 + * @param autoSort 是否自动排序 + * @param connectionName 连接名称 + */ + async sqlRenderPage(sql, query, autoSort = false, connectionName?) { + const { + size = this.conf.size, + page = 1, + order = 'createTime', + sort = 'desc', + isExport = false, + maxExportLimit, + } = query; + if (order && sort && !autoSort) { + if (!(await this.paramSafetyCheck(order + sort))) { + throw new CoolValidateException('非法传参~'); + } + sql += ` ORDER BY ${SqlString.escapeId(order)} ${this.checkSort(sort)}`; + } + if (isExport && maxExportLimit > 0) { + this.sqlParams.push(parseInt(maxExportLimit)); + sql += ' LIMIT ? '; + } + if (!isExport) { + this.sqlParams.push((page - 1) * size); + this.sqlParams.push(parseInt(size)); + sql += ' LIMIT ?,? '; + } + + let params = []; + params = params.concat(this.sqlParams); + const result = await this.nativeQuery(sql, params, connectionName); + const countResult = await this.nativeQuery( + this.getCountSql(sql), + params, + connectionName + ); + return { + list: result, + pagination: { + page: parseInt(page), + size: parseInt(size), + total: parseInt(countResult[0] ? countResult[0].count : 0), + }, + }; + } + + /** + * 设置sql + * @param condition 条件是否成立 + * @param sql sql语句 + * @param params 参数 + */ + setSql(condition, sql, params) { + let rSql = false; + if (condition || (condition === 0 && condition !== '')) { + rSql = true; + this.sqlParams = this.sqlParams.concat(params); + } + return rSql ? sql : ''; + } + + /** + * 获得查询个数的SQL + * @param sql + */ + getCountSql(sql) { + sql = sql.replace('LIMIT ', 'limit '); + return `select count(*) as count from (${ + sql.replace(new RegExp('\n', 'gm'), ' ').split('limit ')[0] + }) a`; + } + + /** + * 参数安全性检查 + * @param params + */ + async paramSafetyCheck(params) { + const lp = params.toLowerCase(); + return !( + lp.indexOf('update ') > -1 || + lp.indexOf('select ') > -1 || + lp.indexOf('delete ') > -1 || + lp.indexOf('insert ') > -1 + ); + } + + /** + * 原生查询 + * @param sql + * @param params + * @param connectionName + */ + async nativeQuery(sql, params?, connectionName?) { + if (_.isEmpty(params)) { + params = this.sqlParams; + } + let newParams = []; + newParams = newParams.concat(params); + this.sqlParams = []; + return await this.getOrmManager(connectionName).query(sql, newParams || []); + } + + /** + * 获得ORM管理 + * @param connectionName 连接名称 + */ + getOrmManager(connectionName = 'default') { + return this.typeORMDataSourceManager.getDataSource(connectionName); + } + + /** + * 非分页查询 + * @param params 查询条件 + */ + async list(params?): Promise { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + const sql = await this.getOptionFind(params, this.curdOption.listQueryOp); + return this.nativeQuery(sql, []); + } + + /** + * 删除 + * @param params 参数 + */ + async delete(params: any | string) { + const { ids } = params; + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + if (ids instanceof Array) { + await this.entity.delete(ids); + } else { + await this.entity.delete(ids.split(',')); + } + await this.modifyAfter(ids); + } + + /** + * 新增|修改 + * @param params 数据 + */ + async addOrUpdate(params: any) { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + await this.entity.save(params); + } + + /** + * 新增 + * @param param 数据 + */ + async add(params: any): Promise { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + await this.addOrUpdate(params); + await this.modifyAfter(params); + return { + id: params.id, + }; + } + + /** + * 修改 + * @param param 数据 + */ + async update(params: any) { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + if (!params.id) throw new CoolValidateException(ERRINFO.NOID); + await this.addOrUpdate(params); + await this.modifyAfter(params); + } + + /** + * 新增|修改|删除 之后的操作 + * @param data 对应数据 + */ + async modifyAfter(data: any): Promise {} + + /** + * 分页查询 + * @param params 查询条件 + */ + async page(params?) { + if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY); + const sql = await this.getOptionFind(params, this.curdOption.pageQueryOp); + return this.sqlRenderPage(sql, params, true); + } + + /** + * query + * @param data + * @param query + */ + renderPage(data, query) { + const { size = this.conf.size, page = 1 } = query; + return { + list: data[0], + pagination: { + page: parseInt(page), + size: parseInt(size), + total: data[1], + }, + }; + } + + /** + * 构建查询配置 + * @param query 前端查询 + * @param option + */ + private async getOptionFind(query, option: QueryOp) { + let { order = 'createTime', sort = 'desc', keyWord = '' } = query || {}; + let sqlArr = ['SELECT']; + let selects = ['a.*']; + let find = this.entity.createQueryBuilder('a'); + if (option) { + // 判断是否有关联查询,有的话取个别名 + if (!_.isEmpty(option.leftJoin)) { + for (const item of option.leftJoin) { + selects.push(`${item.alias}.*`); + find.leftJoin(item.entity, item.alias, item.condition); + } + } + // 默认条件 + if (option.where) { + const wheres = await option.where(query, this.app); + if (!_.isEmpty(wheres)) { + for (const item of wheres) { + if ( + item.length == 2 || + (item.length == 3 && + (item[2] || (item[2] === 0 && item[2] != ''))) + ) { + for (const key in item[1]) { + this.sqlParams.push(item[1][key]); + } + find.andWhere(item[0], item[1]); + } + } + } + } + // 附加排序 + if (!_.isEmpty(option.addOrderBy)) { + for (const key in option.addOrderBy) { + find.addOrderBy( + SqlString.escapeId(key), + this.checkSort(option.addOrderBy[key].toUpperCase()) + ); + } + } + // 关键字模糊搜索 + if (keyWord) { + keyWord = `%${keyWord}%`; + find.andWhere( + new Brackets(qb => { + const keyWordLikeFields = option.keyWordLikeFields; + for (let i = 0; i < option.keyWordLikeFields?.length || 0; i++) { + qb.orWhere(`${keyWordLikeFields[i]} like :keyWord`, { + keyWord, + }); + this.sqlParams.push(keyWord); + } + }) + ); + } + // 筛选字段 + if (!_.isEmpty(option.select)) { + sqlArr.push(option.select.join(',')); + find.select(option.select); + } else { + sqlArr.push(selects.join(',')); + } + // 字段全匹配 + if (!_.isEmpty(option.fieldEq)) { + for (const key of option.fieldEq) { + const c = {}; + // 单表字段无别名的情况下操作 + if (typeof key === 'string') { + if (query[key] || query[key] == 0) { + c[key] = query[key]; + const eq = query[key] instanceof Array ? 'in' : '='; + if (eq === 'in') { + find.andWhere(`${key} ${eq} (:${key})`, c); + } else { + find.andWhere(`${key} ${eq} :${key}`, c); + } + this.sqlParams.push(query[key]); + } + } else { + if (query[key.column] || query[key.column] == 0) { + c[key.column] = query[key.column]; + const eq = query[key.column] instanceof Array ? 'in' : '='; + if (eq === 'in') { + find.andWhere(`${key.column} ${eq} (:${key.column})`, c); + } else { + find.andWhere(`${key.column} ${eq} :${key.column}`, c); + } + this.sqlParams.push(query[key.column]); + } + } + } + } + } else { + sqlArr.push(selects.join(',')); + } + // 接口请求的排序 + if (sort && order) { + const sorts = sort.toUpperCase().split(','); + const orders = order.split(','); + if (sorts.length != orders.length) { + throw new CoolValidateException(ERRINFO.SORTFIELD); + } + for (const i in sorts) { + find.addOrderBy( + SqlString.escapeId(orders[i]), + this.checkSort(sorts[i]) + ); + } + } + const sqls = find.getSql().split('FROM'); + sqlArr.push('FROM'); + sqlArr.push(sqls[1]); + return sqlArr.join(' '); + } +} diff --git a/rpc/src/test.ts b/rpc/src/test.ts new file mode 100644 index 0000000..d15b931 --- /dev/null +++ b/rpc/src/test.ts @@ -0,0 +1,25 @@ +import { Controller, Inject, Post, Provide } from '@midwayjs/decorator'; +import { BaseController } from '@cool-midway/core'; +import { CoolRpc } from './rpc'; + +/** + * 本地开发调试 + */ +@Provide() +@Controller('/rpc') +export class RpcTestController extends BaseController { + @Inject() + rpc: CoolRpc; + + @Inject() + ctx; + + /** + * 测试 + */ + @Post('/test') + async test() { + const { name, service, method, params } = this.ctx.request.body; + return this.rpc.call(name, service, method, params); + } +} diff --git a/rpc/src/transaction/event.ts b/rpc/src/transaction/event.ts new file mode 100644 index 0000000..c81cf6c --- /dev/null +++ b/rpc/src/transaction/event.ts @@ -0,0 +1,40 @@ +import { Logger, Provide, Scope, ScopeEnum } from '@midwayjs/decorator'; +import { CoolRpcEvent, CoolRpcEventHandler } from '..'; +import { ILogger } from '@midwayjs/logger'; + +/** + * moleculer 事件处理 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +@CoolRpcEvent() +export class MoleculerTransactionHandler { + @Logger() + coreLogger: ILogger; + + /** + * 注册事件 + * @param params + */ + @CoolRpcEventHandler('moleculer.transaction') // 唯一参数,eventName,事件名,可不填,默认为方法名 + async handler(params) { + const { rpcTransactionId, commit } = params; + this.coreLogger.info( + `\x1B[36m [cool:core] MoleculerTransaction event params: ${JSON.stringify( + params + )} \x1B[0m` + ); + if (global['moleculer.transactions'][rpcTransactionId]) { + this.coreLogger.info( + `\x1B[36m [cool:core] MoleculerTransaction event ${ + commit ? 'commitTransaction' : 'rollbackTransaction' + } ID: ${rpcTransactionId} \x1B[0m` + ); + await global['moleculer.transactions'][rpcTransactionId][ + commit ? 'commitTransaction' : 'rollbackTransaction' + ](); + await global['moleculer.transactions'][rpcTransactionId].release(); + delete global['moleculer.transactions'][rpcTransactionId]; + } + } +} diff --git a/rpc/tsconfig.json b/rpc/tsconfig.json new file mode 100644 index 0000000..f01e1d2 --- /dev/null +++ b/rpc/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "inlineSourceMap":false, + "noImplicitThis": true, + "noUnusedLocals": true, + "stripInternal": true, + "skipLibCheck": true, + "noImplicitReturns": false, + "pretty": true, + "declaration": true, + "outDir": "dist" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/sms/.editorconfig b/sms/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/sms/.editorconfig @@ -0,0 +1,11 @@ +# 🎨 editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/sms/.eslintrc.json b/sms/.eslintrc.json new file mode 100644 index 0000000..aad9755 --- /dev/null +++ b/sms/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "extends": "./node_modules/mwts/", + "ignorePatterns": [ + "node_modules", + "dist", + "test", + "jest.config.js", + "typings", + "public/**/**", + "view/**/**" + ], + "env": { + "jest": true + }, + "rules": { + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/ban-ts-comment": "off", + "node/no-extraneous-import": "off", + "no-empty": "off", + "node/no-extraneous-require": "off", + "eqeqeq": "off", + "node/no-unsupported-features/node-builtins": "off", + "@typescript-eslint/ban-types": "off", + "no-control-regex": "off", + "prefer-const": "off" + } +} \ No newline at end of file diff --git a/sms/.gitignore b/sms/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/sms/.gitignore @@ -0,0 +1,15 @@ +logs/ +npm-debug.log +yarn-error.log +node_modules/ +package-lock.json +yarn.lock +coverage/ +dist/ +.idea/ +run/ +.DS_Store +*.sw* +*.un~ +.tsbuildinfo +.tsbuildinfo.* diff --git a/sms/.prettierrc.js b/sms/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/sms/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/sms/index.d.ts b/sms/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/sms/index.d.ts @@ -0,0 +1,10 @@ +export * from './dist/index'; + +declare module '@midwayjs/core/dist/interface' { + interface MidwayConfig { + book?: PowerPartial<{ + a: number; + b: string; + }>; + } +} diff --git a/sms/jest.config.js b/sms/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/sms/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], + setupFilesAfterEnv: ['./jest.setup.js'] +}; diff --git a/sms/jest.setup.js b/sms/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/sms/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/sms/package.json b/sms/package.json new file mode 100644 index 0000000..4911196 --- /dev/null +++ b/sms/package.json @@ -0,0 +1,48 @@ +{ + "name": "@cool-midway/sms", + "version": "6.0.1", + "description": "cool-js.com 短信", + "main": "dist/index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [ + "cool", + "cool-admin", + "cooljs" + ], + "author": "COOL", + "readme": "README.md", + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts", + "index.d.ts" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^2.0.9", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@types/jest": "^29.2.5", + "@types/node": "^18.11.18", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typescript": "^4.9.4" + }, + "dependencies": { + "@alicloud/pop-core": "^1.7.13", + "tencentcloud-sdk-nodejs": "^4.0.607" + } +} diff --git a/sms/src/ali.ts b/sms/src/ali.ts new file mode 100644 index 0000000..a2f4e6f --- /dev/null +++ b/sms/src/ali.ts @@ -0,0 +1,64 @@ +import * as Core from '@alicloud/pop-core'; +import { Config, Provide } from "@midwayjs/core"; +import { CoolSmsAliConfig } from './interface'; +import { CoolCommException } from '@cool-midway/core'; + +/** + * 阿里云短信 + */ +@Provide() +export class SmsAli { + @Config('cool.sms.ali') + config: CoolSmsAliConfig; + + /** + * 配置 + * @param config + */ + setConfig(config: CoolSmsAliConfig) { + this.config = config; + } + + /** + * 发送短信 + * @param phone 手机号 + * @param params 参数 + * @param config signName 签名 template 模板 + * @returns + */ + async send(phone, params: { + [key: string]: string; + }, config?: { + signName: string; + template: string; + }) { + const { accessKeyId, accessKeySecret } = this.config; + if (!accessKeyId || !accessKeyId) { + throw new CoolCommException('请配置阿里云短信'); + } + if (!config) { + config = { + signName: this.config.signName, + template: this.config.template, + }; + } + const client = new Core({ + accessKeyId, + accessKeySecret, + endpoint: 'https://dysmsapi.aliyuncs.com', + // endpoint: 'https://cs.cn-hangzhou.aliyuncs.com', + apiVersion: '2017-05-25', + // apiVersion: '2018-04-18', + }); + const data = { + RegionId: 'cn-shanghai', + PhoneNumbers: phone, + signName: config.signName, + templateCode: config.template, + TemplateParam: JSON.stringify(params), + }; + return await client.request('SendSms', data, { + method: 'POST', + }); + } +} \ No newline at end of file diff --git a/sms/src/config/config.default.ts b/sms/src/config/config.default.ts new file mode 100644 index 0000000..2a8c538 --- /dev/null +++ b/sms/src/config/config.default.ts @@ -0,0 +1,5 @@ +export const customKey = { + a: 1, + b: 'hello', + }; + \ No newline at end of file diff --git a/sms/src/configuration.ts b/sms/src/configuration.ts new file mode 100644 index 0000000..d968e7f --- /dev/null +++ b/sms/src/configuration.ts @@ -0,0 +1,17 @@ +import { Configuration } from '@midwayjs/decorator'; +import * as DefaultConfig from './config/config.default'; +import { IMidwayContainer } from '@midwayjs/core'; + +@Configuration({ + namespace: 'cool:sms', + importConfigs: [ + { + default: DefaultConfig, + }, + ], +}) +export class CoolPayConfiguration { + async onReady(container: IMidwayContainer) { + // TODO something + } +} diff --git a/sms/src/index.ts b/sms/src/index.ts new file mode 100644 index 0000000..3856a5c --- /dev/null +++ b/sms/src/index.ts @@ -0,0 +1,9 @@ +export { CoolPayConfiguration as Configuration } from './configuration'; + +export * from './interface'; + +export * from './ali'; +export * from './yp'; +export * from './tx'; + +export * from './sms'; diff --git a/sms/src/interface.ts b/sms/src/interface.ts new file mode 100644 index 0000000..cad710a --- /dev/null +++ b/sms/src/interface.ts @@ -0,0 +1,80 @@ +export interface CoolSmsConfig { + /** + * 阿里云短信配置 + */ + ali: CoolSmsAliConfig; + /** + * 腾讯云短信配置 + */ + tx: CoolTxConfig; + /** + * 云片短信配置 + */ + yp: CoolYpConfig; +} + +/** + * 阿里云配置 + */ +export interface CoolSmsAliConfig { + /** + * 阿里云accessKeyId + */ + accessKeyId: string; + /** + * 阿里云accessKeySecret + */ + accessKeySecret: string; + /** + * 签名,非必填,调用时可以传入 + */ + signName?: string; + /** + * 模板,非必填,调用时可以传入 + */ + template?: string; +} + +/** + * 腾讯云配置 + */ +export interface CoolTxConfig { + /** + * 应用ID + */ + appId: string; + /** + * 腾讯云secretId + */ + secretId: string; + /** + * 腾讯云secretKey + */ + secretKey: string; + /** + * 签名,非必填,调用时可以传入 + */ + signName?: string; + /** + * 模板,非必填,调用时可以传入 + */ + template?: string; +} + +/** + * 云片短信配置 + */ +export interface CoolYpConfig { + /** + * 云片apikey + */ + apikey: string; + /** + * 签名,非必填,调用时可以传入 + */ + signName?: string; + /** + * 模板,非必填,调用时可以传入 + */ + template?: string; +} diff --git a/sms/src/package.json b/sms/src/package.json new file mode 100644 index 0000000..7f2c3ac --- /dev/null +++ b/sms/src/package.json @@ -0,0 +1,48 @@ +{ + "name": "@cool-midway/sms", + "version": "6.0.1", + "description": "cool-js.com 短信", + "main": "index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [ + "cool", + "cool-admin", + "cooljs" + ], + "author": "COOL", + "readme": "README.md", + "files": [ + "**/*.js", + "**/*.d.ts", + "index.d.ts" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^2.0.9", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@types/jest": "^29.2.5", + "@types/node": "^18.11.18", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typescript": "^4.9.4" + }, + "dependencies": { + "@alicloud/pop-core": "^1.7.13", + "tencentcloud-sdk-nodejs": "^4.0.607" + } +} diff --git a/sms/src/sms.ts b/sms/src/sms.ts new file mode 100644 index 0000000..2ce7b3d --- /dev/null +++ b/sms/src/sms.ts @@ -0,0 +1,83 @@ +import { Config, Inject, Provide } from "@midwayjs/core"; +import { SmsYp } from "./yp"; +import { SmsAli } from "./ali"; +import { SmsTx } from "./tx"; +import { CoolSmsConfig } from "./interface"; + +@Provide() +export class CoolSms { + @Inject() + smsYp: SmsYp + + @Inject() + smsAli: SmsAli + + @Inject() + smsTx: SmsTx + + @Config('cool.sms') + config: CoolSmsConfig; + + /** + * 配置 + * @param config + */ + setConfig(config: CoolSmsConfig) { + this.smsYp.setConfig(config.yp); + this.smsAli.setConfig(config.ali); + this.smsTx.setConfig(config.tx); + this.config = config; + } + + /** + * 发送验证码 模板字段名为:code + * @param phone + * @param config + */ + async sendCode(phone, config?: { + signName: string; + template: string; + }) { + const code = this.generateNumber(); + let params = { + code + } + await this.send(phone, this.config.tx ? [code] : params, config) + return code; + } + + /** + * 发送短信 + * @param phone + * @param params + * @param config + * @returns + */ + async send(phone: string, params: any, config?: { + signName: string; + template: string; + }) { + if (this.config.ali) { + return await this.smsAli.send(phone, params, config); + } + if (this.config.tx) { + return await this.smsTx.send(phone, params, config); + } + if (this.config.yp) { + return await this.smsYp.send(phone, params, config); + } + return true; + } + + /** + * 生成验证码 + */ + generateNumber(digits = 4) { + if (digits <= 0) { + return 0; + } + const min = Math.pow(10, digits - 1); + const max = Math.pow(10, digits) - 1; + return Math.floor(Math.random() * (max - min + 1)) + min; + } +} \ No newline at end of file diff --git a/sms/src/tx.ts b/sms/src/tx.ts new file mode 100644 index 0000000..1d3e9e1 --- /dev/null +++ b/sms/src/tx.ts @@ -0,0 +1,70 @@ +import { Config, Provide } from "@midwayjs/core"; +import { CoolTxConfig } from './interface'; +import * as tencentcloud from "tencentcloud-sdk-nodejs"; +import { CoolCommException } from "@cool-midway/core"; + +/** + * 腾讯云短信 + */ +@Provide() +export class SmsTx { + @Config('cool.sms.tx') + config: CoolTxConfig; + + /** + * 配置 + * @param config + */ + setConfig(config: CoolTxConfig) { + this.config = config; + } + + /** + * 发送短信 + * @param phone 手机号 + * @param params 参数 + * @param config signName 签名 template 模板 + * @returns + */ + async send(phone: string, params: string[], config?: { + signName: string; + template: string; + }) { + const { appId, secretId, secretKey } = this.config; + if(!config) { + config = { + signName: this.config.signName, + template: this.config.template, + }; + } + if(!appId || !secretId || !secretKey) { + throw new CoolCommException('请配置腾讯云短信'); + } + const smsClient = tencentcloud.sms.v20210111.Client; + + const client = new smsClient({ + credential: { + secretId, + secretKey, + }, + region: 'ap-guangzhou', + profile: { + signMethod: 'HmacSHA256', + httpProfile: { + reqMethod: 'POST', + reqTimeout: 30, + endpoint: 'sms.tencentcloudapi.com', + }, + }, + }); + + const data = { + SmsSdkAppId: appId, + SignName: config.signName, + TemplateId: config.template, + TemplateParamSet: params, + PhoneNumberSet: [`+86${phone}`], + }; + return client.SendSms(data); + } +} \ No newline at end of file diff --git a/sms/src/yp.ts b/sms/src/yp.ts new file mode 100644 index 0000000..01ffc66 --- /dev/null +++ b/sms/src/yp.ts @@ -0,0 +1,86 @@ +import { Config, Provide } from "@midwayjs/core"; +import { CoolYpConfig } from "./interface"; +import { CoolCommException } from "@cool-midway/core"; +import axios from 'axios'; + +/** + * 云片短信 + */ +@Provide() +export class SmsYp { + @Config('cool.sms.yp') + config: CoolYpConfig; + + /** + * 配置 + * @param config + */ + setConfig(config: CoolYpConfig) { + this.config = config; + } + + /** + * 发送短信 + * @param phones 手机号 数组,需要加国家码如 ["+8612345678901"] + * @param params 参数 + * @param config signName 签名 template 模板 + * @returns + */ + async send(phones: string, params: { + [key: string]: string; + }, config?: { + signName: string; + template: string; + }) { + const { apikey } = this.config; + if (!config) { + config = { + signName: this.config.signName, + template: this.config.template, + }; + } + if (!apikey) { + throw new CoolCommException('请配置云片短信'); + } + + const headers = { + Accept: 'application/json;charset=utf-8', + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }; + const data = { + apikey: apikey, + mobile: phones, + tpl_id: config.template, + tpl_value: this.smsTplValue(params), + }; + const result = await axios.post( + 'https://sms.yunpian.com/v2/sms/tpl_single_send.json', + data, + { headers } + ); + if (result.data.code === 0) { + return true; + } + return false; + } + + /** + * 获得短信模板值 + * @param obj + * @returns + */ + protected smsTplValue(obj) { + const urlParams = []; + + for (let key in obj) { + // eslint-disable-next-line no-prototype-builtins + if (obj.hasOwnProperty(key)) { + const encodedKey = encodeURIComponent(`#${key}#`); + const encodedValue = encodeURIComponent(obj[key]); + urlParams.push(`${encodedKey}=${encodedValue}`); + } + } + + return urlParams.join('&'); + } +} \ No newline at end of file diff --git a/sms/tsconfig.json b/sms/tsconfig.json new file mode 100644 index 0000000..f01e1d2 --- /dev/null +++ b/sms/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "inlineSourceMap":false, + "noImplicitThis": true, + "noUnusedLocals": true, + "stripInternal": true, + "skipLibCheck": true, + "noImplicitReturns": false, + "pretty": true, + "declaration": true, + "outDir": "dist" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/task/.editorconfig b/task/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/task/.editorconfig @@ -0,0 +1,11 @@ +# 🎨 editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/task/.eslintrc.json b/task/.eslintrc.json new file mode 100644 index 0000000..fa5dee4 --- /dev/null +++ b/task/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "extends": "./node_modules/mwts/", + "ignorePatterns": [ + "node_modules", + "dist", + "test", + "jest.config.js", + "typings", + "public/**/**", + "view/**/**" + ], + "env": { + "jest": true + }, + "rules": { + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/ban-ts-comment": "off", + "node/no-extraneous-import": "off", + "no-empty": "off", + "node/no-extraneous-require": "off", + "eqeqeq": "off", + "node/no-unsupported-features/node-builtins": "off", + "@typescript-eslint/ban-types": "off", + "no-control-regex": "off", + "prefer-const": "off" + } +} \ No newline at end of file diff --git a/task/.gitignore b/task/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/task/.gitignore @@ -0,0 +1,15 @@ +logs/ +npm-debug.log +yarn-error.log +node_modules/ +package-lock.json +yarn.lock +coverage/ +dist/ +.idea/ +run/ +.DS_Store +*.sw* +*.un~ +.tsbuildinfo +.tsbuildinfo.* diff --git a/task/.prettierrc.js b/task/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/task/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/task/README.md b/task/README.md new file mode 100644 index 0000000..610c3db --- /dev/null +++ b/task/README.md @@ -0,0 +1,3 @@ +# cool-admin + +https://cool-js.com diff --git a/task/index.d.ts b/task/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/task/index.d.ts @@ -0,0 +1,10 @@ +export * from './dist/index'; + +declare module '@midwayjs/core/dist/interface' { + interface MidwayConfig { + book?: PowerPartial<{ + a: number; + b: string; + }>; + } +} diff --git a/task/jest.config.js b/task/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/task/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], + setupFilesAfterEnv: ['./jest.setup.js'] +}; diff --git a/task/jest.setup.js b/task/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/task/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/task/package.json b/task/package.json new file mode 100644 index 0000000..ae42614 --- /dev/null +++ b/task/package.json @@ -0,0 +1,50 @@ +{ + "name": "@cool-midway/task", + "version": "6.0.0", + "description": "cool-js.com 任务与队列", + "main": "dist/index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [ + "cool", + "cool-admin", + "cooljs" + ], + "author": "COOL", + "readme": "README.md", + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts", + "index.d.ts" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^2.0.9", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@midwayjs/redis": "^3.9.0", + "@types/jest": "^29.2.5", + "@types/node": "^18.11.18", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "lodash": "^4.17.21", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typescript": "^4.9.4" + }, + "dependencies": { + "bullmq": "^3.5.2", + "ioredis": "^5.2.4" + } +} diff --git a/task/src/base.ts b/task/src/base.ts new file mode 100644 index 0000000..8d1e5b6 --- /dev/null +++ b/task/src/base.ts @@ -0,0 +1,118 @@ +import { + Job, + JobsOptions, + Queue, + QueueGetters, + RepeatOptions, + Worker, +} from 'bullmq'; + +/** + * 队列基类 + */ +export abstract class BaseCoolQueue { + /** + * @deprecated 将在后续版本废弃 + */ + queue: BaseCoolQueue; + // 获得者 + getters: QueueGetters; + // 消费者 + worker: Worker; + // 队列名 + queueName: string; + // 原始队列 + metaQueue: Queue; + + constructor() { + this.queue = this; + } + + // 数据 + async data(job: Job, done: Function) {} + + /** + * 发送数据 + * @param data + * @param opts + */ + async add(data: any, opts?: JobsOptions): Promise> { + return this.metaQueue.add(this.queueName, data, opts); + } + + /** + * 批量新增 + * @param datas + * @param opts + */ + async addBulk( + datas: any[], + opts?: JobsOptions + ): Promise[]> { + return this.metaQueue.addBulk( + datas.map(data => { + return { + name: this.queueName, + data, + opts, + }; + }) + ); + } + + defaultJobOptions(): JobsOptions { + return this.metaQueue.defaultJobOptions; + } + + async repeat() { + return this.metaQueue.repeat; + } + + async pause() { + this.metaQueue.pause(); + } + + async resume() { + this.metaQueue.resume(); + } + + async isPaused() { + return this.metaQueue.isPaused(); + } + + async getRepeatableJobs(start?: number, end?: number, asc?: boolean) { + return this.metaQueue.getRepeatableJobs(start, end, asc); + } + + async removeRepeatable(repeatOpts: RepeatOptions, jobId?: string) { + this.metaQueue.removeRepeatable(this.queueName, repeatOpts, jobId); + } + + async removeRepeatableByKey(key: string) { + this.metaQueue.removeRepeatableByKey(key); + } + + async remove(jobId: string) { + return this.metaQueue.remove(jobId); + } + + async drain(delayed?: boolean) { + this.metaQueue.drain(delayed); + } + + async clean( + grace: number, + limit: number, + type?: 'completed' | 'wait' | 'active' | 'paused' | 'delayed' | 'failed' + ) { + return this.metaQueue.clean(grace, limit, type); + } + + async obliterate(opts?: { force?: boolean; count?: number }) { + this.metaQueue.obliterate(opts); + } + + async trimEvents(maxLength: number) { + return this.metaQueue.trimEvents(maxLength); + } +} diff --git a/task/src/config/config.default.ts b/task/src/config/config.default.ts new file mode 100644 index 0000000..a30c406 --- /dev/null +++ b/task/src/config/config.default.ts @@ -0,0 +1,6 @@ +/** + * cool的配置 + */ +export default { + cool: {}, +}; diff --git a/task/src/configuration.ts b/task/src/configuration.ts new file mode 100644 index 0000000..4baebdf --- /dev/null +++ b/task/src/configuration.ts @@ -0,0 +1,19 @@ +import { Configuration } from '@midwayjs/decorator'; +import * as DefaultConfig from './config/config.default'; +import { IMidwayContainer } from '@midwayjs/core'; +import { CoolQueueHandle } from './queue'; + +@Configuration({ + namespace: 'cool:task', + importConfigs: [ + { + default: DefaultConfig, + }, + ], +}) +export class CoolTaskConfiguration { + async onReady(container: IMidwayContainer) { + await container.getAsync(CoolQueueHandle); + // TODO something + } +} diff --git a/task/src/decorator/queue.ts b/task/src/decorator/queue.ts new file mode 100644 index 0000000..6303fb4 --- /dev/null +++ b/task/src/decorator/queue.ts @@ -0,0 +1,26 @@ +import { + Scope, + ScopeEnum, + saveClassMetadata, + saveModule, +} from '@midwayjs/decorator'; +import { JobsOptions } from 'bullmq'; + +export const COOL_TASK_KEY = 'decorator:cool:task'; + +export function CoolQueue( + config = { type: 'comm', queue: {}, worker: {} } as { + type?: 'comm' | 'getter' | 'noworker' | 'single'; + queue?: JobsOptions; + worker?: WorkerOptions; + } +): ClassDecorator { + return (target: any) => { + // 将装饰的类,绑定到该装饰器,用于后续能获取到 class + saveModule(COOL_TASK_KEY, target); + // 保存一些元数据信息,任意你希望存的东西 + saveClassMetadata(COOL_TASK_KEY, config, target); + // 指定 IoC 容器创建实例的作用域,这里注册为请求作用域,这样能取到 ctx + Scope(ScopeEnum.Singleton)(target); + }; +} diff --git a/task/src/index.ts b/task/src/index.ts new file mode 100644 index 0000000..37134d7 --- /dev/null +++ b/task/src/index.ts @@ -0,0 +1,7 @@ +export { CoolTaskConfiguration as Configuration } from './configuration'; + +export * from './base'; + +export * from './queue'; + +export * from './decorator/queue'; diff --git a/task/src/package.json b/task/src/package.json new file mode 100644 index 0000000..8f52a4c --- /dev/null +++ b/task/src/package.json @@ -0,0 +1,50 @@ +{ + "name": "@cool-midway/task", + "version": "6.0.0", + "description": "cool-js.com 任务与队列", + "main": "index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "cov": "cross-env midway-bin cov --ts", + "lint": "mwts check", + "lint:fix": "mwts fix" + }, + "keywords": [ + "cool", + "cool-admin", + "cooljs" + ], + "author": "COOL", + "readme": "README.md", + "files": [ + "**/*.js", + "**/*.d.ts", + "index.d.ts" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://cool-js.com" + }, + "devDependencies": { + "@cool-midway/core": "^6.0.0", + "@midwayjs/cli": "^2.0.9", + "@midwayjs/core": "^3.9.0", + "@midwayjs/decorator": "^3.9.0", + "@midwayjs/mock": "^3.9.0", + "@midwayjs/redis": "^3.9.0", + "@types/jest": "^29.2.5", + "@types/node": "^18.11.18", + "cross-env": "^7.0.3", + "jest": "^29.3.1", + "lodash": "^4.17.21", + "mwts": "^1.3.0", + "ts-jest": "^29.0.3", + "typescript": "^4.9.4" + }, + "dependencies": { + "bullmq": "^3.5.2", + "ioredis": "^5.2.4" + } +} diff --git a/task/src/queue.ts b/task/src/queue.ts new file mode 100644 index 0000000..533a824 --- /dev/null +++ b/task/src/queue.ts @@ -0,0 +1,142 @@ +import { ILogger, IMidwayApplication } from '@midwayjs/core'; +import { + App, + Config, + getClassMetadata, + Init, + listModule, + Logger, + Provide, + Scope, + ScopeEnum, +} from '@midwayjs/decorator'; +import { Job, QueueGetters, Queue, Worker } from 'bullmq'; +import { BaseCoolQueue } from './base'; +import { COOL_TASK_KEY } from './decorator/queue'; +import Redis from 'ioredis'; + +/** + * 任务队列 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolQueueHandle { + @Config('cool.redis') + redisConfig; + + @Logger() + coreLogger: ILogger; + + @App() + app: IMidwayApplication; + + redis; + + @Init() + async init() { + if (!this.redisConfig) { + this.coreLogger.error('@cool-midway/task组件 redis未配置'); + } + + await this.scan(); + } + + /** + * 扫描队列 + */ + async scan() { + const modules = listModule(COOL_TASK_KEY); + for (let mod of modules) { + const cls: BaseCoolQueue = await this.app + .getApplicationContext() + .getAsync(mod); + this.createQueue(cls, mod); + } + } + + /** + * 获得锁 + * @param key 键 + * @param expireTime 过期时间 + * @returns + */ + async getLock(key, expireTime) { + const lockSuccessful = await this.redis.setnx(key, 'locked'); + if (lockSuccessful) { + await this.redis.expire(key, expireTime); + return true; + } else { + return false; + } + } + + /** + * 队列名称 + * @param cls + * @param mod + */ + async createQueue(cls: BaseCoolQueue, mod: any) { + this.redis; + if (this.redisConfig instanceof Array) { + this.redis = new Redis.Cluster(this.redisConfig, { + enableReadyCheck: false, + }); + } else { + this.redis = new Redis({ + ...this.redisConfig, + enableReadyCheck: false, + maxRetriesPerRequest: null, + }); + } + const name = mod.name; + const config = getClassMetadata(COOL_TASK_KEY, mod); + const opts = { + connection: this.redis, + prefix: `{queue${name}}`, + defaultJobOptions: { + removeOnComplete: true, + removeOnFail: true, + attempts: 5, + backoff: { + type: 'fixed', + delay: 10000, + }, + ...(config.queue || {}), + }, + }; + const queue = new Queue(name, opts); + cls.metaQueue = queue; + cls.queueName = name; + let lock = false; + // 本地开发的情况下直接获得锁 + if (config.type == 'single') { + if (this.app.getEnv() == 'local') { + lock = true; + } else { + // cluster 需要配合redis 获得锁 + if (await this.getLock('COOL_QUEUE_SINGLE', 15)) { + lock = true; + } + } + } + + if (config.type == 'comm' || (config.type == 'single' && lock)) { + cls.worker = new Worker( + name, + async (job: Job) => { + await cls.data(job, async () => { + await job.isCompleted(); + }); + }, + { + connection: opts.connection, + prefix: opts.prefix, + ...(config.worker || {}), + } + ); + } else { + cls.getters = new QueueGetters(name, opts); + } + this.coreLogger.info(`\x1B[36m [cool:task] create ${name} queue \x1B[0m`); + } +} diff --git a/task/tsconfig.json b/task/tsconfig.json new file mode 100644 index 0000000..f01e1d2 --- /dev/null +++ b/task/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "inlineSourceMap":false, + "noImplicitThis": true, + "noUnusedLocals": true, + "stripInternal": true, + "skipLibCheck": true, + "noImplicitReturns": false, + "pretty": true, + "declaration": true, + "outDir": "dist" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4ec9192 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "inlineSourceMap":true, + "noImplicitThis": true, + "noUnusedLocals": true, + "stripInternal": true, + "skipLibCheck": true, + "pretty": true, + "declaration": true, + "noImplicitAny": false, + "typeRoots": [ "./typings", "./node_modules/@types"], + "outDir": "dist" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +}