From 600e2396c4536badf73bee6072056327d43e1fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=95=8A=E5=B9=B3?= <951984189@qq.com> Date: Tue, 4 Apr 2023 09:15:48 +0800 Subject: [PATCH] =?UTF-8?q?npm=20=E6=A0=B8=E5=BF=83=E5=8C=85=E6=BA=90?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cloud/.editorconfig | 11 + packages/cloud/.eslintrc.json | 20 + packages/cloud/.gitignore | 15 + packages/cloud/.prettierrc.js | 3 + packages/cloud/LICENSE | 21 + packages/cloud/index.d.ts | 10 + packages/cloud/jest.config.js | 7 + packages/cloud/jest.setup.js | 1 + packages/cloud/package.json | 47 ++ packages/cloud/src/LICENSE | 21 + packages/cloud/src/config/config.default.ts | 4 + packages/cloud/src/configuration.ts | 22 + packages/cloud/src/db/index.ts | 123 ++++ packages/cloud/src/db/source.ts | 10 + packages/cloud/src/func/crud.ts | 525 ++++++++++++++++ packages/cloud/src/func/index.ts | 17 + packages/cloud/src/index.ts | 10 + packages/cloud/src/interface.ts | 11 + packages/cloud/src/package.json | 42 ++ packages/cloud/src/util.ts | 1 + packages/cloud/test/index.test.ts | 14 + packages/cloud/tsconfig.json | 25 + packages/core/LICENSE | 21 + packages/core/README.md | 3 + packages/core/_.editorconfig | 11 + packages/core/_.eslintrc.json | 7 + packages/core/_.gitignore | 15 + packages/core/_.prettierrc.js | 3 + packages/core/index.d.ts | 10 + packages/core/jest.config.js | 7 + packages/core/jest.setup.js | 1 + packages/core/package.json | 56 ++ packages/core/src/LICENSE | 21 + packages/core/src/config/config.default.ts | 18 + packages/core/src/configuration.ts | 82 +++ packages/core/src/constant/global.ts | 50 ++ packages/core/src/controller/base.ts | 201 +++++++ packages/core/src/decorator/cache.ts | 8 + packages/core/src/decorator/controller.ts | 208 +++++++ packages/core/src/decorator/event.ts | 42 ++ packages/core/src/decorator/index.ts | 103 ++++ packages/core/src/decorator/tag.ts | 27 + packages/core/src/decorator/transaction.ts | 19 + packages/core/src/entity/base.ts | 27 + packages/core/src/entity/mongo.ts | 24 + packages/core/src/entity/typeorm.ts | 3 + packages/core/src/event/index.ts | 43 ++ packages/core/src/exception/base.ts | 13 + packages/core/src/exception/comm.ts | 15 + packages/core/src/exception/core.ts | 15 + packages/core/src/exception/filter.ts | 20 + packages/core/src/exception/validate.ts | 15 + packages/core/src/index.ts | 45 ++ packages/core/src/interface.ts | 300 +++++++++ packages/core/src/module/config.ts | 100 +++ packages/core/src/module/import.ts | 150 +++++ packages/core/src/package.json | 55 ++ packages/core/src/rest/eps.ts | 124 ++++ packages/core/src/service/base.ts | 526 ++++++++++++++++ packages/core/src/tag/data.ts | 46 ++ packages/core/src/util/func.ts | 27 + packages/core/src/util/location.ts | 95 +++ packages/core/test/index.test.ts | 14 + packages/core/tsconfig.json | 25 + packages/es/.editorconfig | 11 + packages/es/.eslintrc.json | 28 + packages/es/.gitignore | 15 + packages/es/.prettierrc.js | 3 + packages/es/README.md | 3 + packages/es/index.d.ts | 10 + packages/es/jest.config.js | 7 + packages/es/jest.setup.js | 1 + packages/es/package.json | 44 ++ packages/es/src/base.ts | 569 ++++++++++++++++++ packages/es/src/config/config.default.ts | 4 + packages/es/src/configuration.ts | 19 + packages/es/src/decorator/elasticsearch.ts | 41 ++ packages/es/src/elasticsearch.ts | 154 +++++ packages/es/src/index.ts | 15 + packages/es/src/package.json | 42 ++ packages/es/test/index.test.ts | 14 + packages/es/tsconfig.json | 24 + packages/file/.editorconfig | 11 + packages/file/.eslintrc.json | 20 + packages/file/.gitignore | 15 + packages/file/.prettierrc.js | 3 + packages/file/LICENSE | 21 + packages/file/index.d.ts | 10 + packages/file/jest.config.js | 7 + packages/file/jest.setup.js | 1 + packages/file/package.json | 53 ++ packages/file/src/config/config.default.ts | 16 + packages/file/src/configuration.ts | 21 + packages/file/src/file.ts | 475 +++++++++++++++ packages/file/src/index.ts | 5 + packages/file/src/interface.ts | 103 ++++ packages/file/src/package.json | 53 ++ packages/file/test/index.test.ts | 14 + packages/file/tsconfig.json | 25 + packages/iot/.editorconfig | 11 + packages/iot/.eslintrc.json | 28 + packages/iot/.gitignore | 15 + packages/iot/.prettierrc.js | 3 + packages/iot/README.md | 3 + packages/iot/index.d.ts | 10 + packages/iot/jest.config.js | 7 + packages/iot/jest.setup.js | 1 + packages/iot/package.json | 53 ++ packages/iot/src/config/config.default.ts | 13 + packages/iot/src/configuration.ts | 18 + packages/iot/src/decorator/mqtt.ts | 42 ++ packages/iot/src/index.ts | 7 + packages/iot/src/interface.ts | 34 ++ packages/iot/src/mqtt.ts | 163 +++++ packages/iot/src/package.json | 53 ++ packages/iot/test/index.test.ts | 14 + packages/iot/tsconfig.json | 24 + .../mqemitter-redis/.github/dependabot.yml | 7 + .../mqemitter-redis/.github/workflows/ci.yml | 35 ++ packages/other/mqemitter-redis/.gitignore | 33 + packages/other/mqemitter-redis/.travis.yml | 9 + packages/other/mqemitter-redis/LICENSE | 21 + packages/other/mqemitter-redis/README.md | 76 +++ .../other/mqemitter-redis/mqemitter-redis.js | 250 ++++++++ packages/other/mqemitter-redis/package.json | 46 ++ packages/other/mqemitter-redis/test.js | 116 ++++ .../other/mqemitter-redis/types/index.d.ts | 37 ++ .../mqemitter-redis/types/index.test-d.ts | 42 ++ packages/pay/.editorconfig | 11 + packages/pay/.eslintrc.json | 28 + packages/pay/.gitignore | 15 + packages/pay/.prettierrc.js | 3 + packages/pay/index.d.ts | 10 + packages/pay/jest.config.js | 7 + packages/pay/jest.setup.js | 1 + packages/pay/package.json | 50 ++ packages/pay/src/ali.ts | 56 ++ packages/pay/src/config/config.default.ts | 4 + packages/pay/src/configuration.ts | 21 + packages/pay/src/index.ts | 7 + packages/pay/src/interface.ts | 77 +++ packages/pay/src/package.json | 50 ++ packages/pay/src/wx.ts | 68 +++ packages/pay/test/index.test.ts | 14 + packages/pay/tsconfig.json | 24 + packages/rpc/.editorconfig | 11 + packages/rpc/.eslintrc.json | 29 + packages/rpc/.gitignore | 15 + packages/rpc/.prettierrc.js | 3 + packages/rpc/index.d.ts | 10 + packages/rpc/jest.config.js | 7 + packages/rpc/jest.setup.js | 1 + packages/rpc/package.json | 48 ++ packages/rpc/src/config/config.default.ts | 6 + packages/rpc/src/configuration.ts | 26 + packages/rpc/src/decorator/event/event.ts | 19 + packages/rpc/src/decorator/event/handler.ts | 23 + packages/rpc/src/decorator/index.ts | 101 ++++ packages/rpc/src/decorator/rpc.ts | 84 +++ packages/rpc/src/decorator/transaction.ts | 22 + packages/rpc/src/index.ts | 25 + packages/rpc/src/package.json | 48 ++ packages/rpc/src/rpc.ts | 274 +++++++++ packages/rpc/src/service/base.ts | 406 +++++++++++++ packages/rpc/src/test.ts | 25 + packages/rpc/src/transaction/event.ts | 40 ++ packages/rpc/test/index.test.ts | 14 + packages/rpc/tsconfig.json | 24 + packages/task/.editorconfig | 11 + packages/task/.eslintrc.json | 28 + packages/task/.gitignore | 15 + packages/task/.prettierrc.js | 3 + packages/task/README.md | 3 + packages/task/index.d.ts | 10 + packages/task/jest.config.js | 7 + packages/task/jest.setup.js | 1 + packages/task/package.json | 51 ++ packages/task/src/base.ts | 118 ++++ packages/task/src/config/config.default.ts | 6 + packages/task/src/configuration.ts | 19 + packages/task/src/decorator/queue.ts | 26 + packages/task/src/index.ts | 7 + packages/task/src/package.json | 51 ++ packages/task/src/queue.ts | 142 +++++ packages/task/test/index.test.ts | 14 + packages/task/tsconfig.json | 24 + 186 files changed, 8826 insertions(+) create mode 100644 packages/cloud/.editorconfig create mode 100644 packages/cloud/.eslintrc.json create mode 100644 packages/cloud/.gitignore create mode 100644 packages/cloud/.prettierrc.js create mode 100644 packages/cloud/LICENSE create mode 100644 packages/cloud/index.d.ts create mode 100644 packages/cloud/jest.config.js create mode 100644 packages/cloud/jest.setup.js create mode 100644 packages/cloud/package.json create mode 100644 packages/cloud/src/LICENSE create mode 100644 packages/cloud/src/config/config.default.ts create mode 100644 packages/cloud/src/configuration.ts create mode 100644 packages/cloud/src/db/index.ts create mode 100644 packages/cloud/src/db/source.ts create mode 100644 packages/cloud/src/func/crud.ts create mode 100644 packages/cloud/src/func/index.ts create mode 100644 packages/cloud/src/index.ts create mode 100644 packages/cloud/src/interface.ts create mode 100644 packages/cloud/src/package.json create mode 100644 packages/cloud/src/util.ts create mode 100644 packages/cloud/test/index.test.ts create mode 100644 packages/cloud/tsconfig.json create mode 100644 packages/core/LICENSE create mode 100644 packages/core/README.md create mode 100644 packages/core/_.editorconfig create mode 100644 packages/core/_.eslintrc.json create mode 100644 packages/core/_.gitignore create mode 100644 packages/core/_.prettierrc.js create mode 100644 packages/core/index.d.ts create mode 100644 packages/core/jest.config.js create mode 100644 packages/core/jest.setup.js create mode 100644 packages/core/package.json create mode 100644 packages/core/src/LICENSE create mode 100644 packages/core/src/config/config.default.ts create mode 100644 packages/core/src/configuration.ts create mode 100644 packages/core/src/constant/global.ts create mode 100644 packages/core/src/controller/base.ts create mode 100644 packages/core/src/decorator/cache.ts create mode 100644 packages/core/src/decorator/controller.ts create mode 100644 packages/core/src/decorator/event.ts create mode 100644 packages/core/src/decorator/index.ts create mode 100644 packages/core/src/decorator/tag.ts create mode 100644 packages/core/src/decorator/transaction.ts create mode 100644 packages/core/src/entity/base.ts create mode 100644 packages/core/src/entity/mongo.ts create mode 100644 packages/core/src/entity/typeorm.ts create mode 100644 packages/core/src/event/index.ts create mode 100644 packages/core/src/exception/base.ts create mode 100644 packages/core/src/exception/comm.ts create mode 100644 packages/core/src/exception/core.ts create mode 100644 packages/core/src/exception/filter.ts create mode 100644 packages/core/src/exception/validate.ts create mode 100644 packages/core/src/index.ts create mode 100644 packages/core/src/interface.ts create mode 100644 packages/core/src/module/config.ts create mode 100644 packages/core/src/module/import.ts create mode 100644 packages/core/src/package.json create mode 100644 packages/core/src/rest/eps.ts create mode 100644 packages/core/src/service/base.ts create mode 100644 packages/core/src/tag/data.ts create mode 100644 packages/core/src/util/func.ts create mode 100644 packages/core/src/util/location.ts create mode 100644 packages/core/test/index.test.ts create mode 100644 packages/core/tsconfig.json create mode 100644 packages/es/.editorconfig create mode 100644 packages/es/.eslintrc.json create mode 100644 packages/es/.gitignore create mode 100644 packages/es/.prettierrc.js create mode 100644 packages/es/README.md create mode 100644 packages/es/index.d.ts create mode 100644 packages/es/jest.config.js create mode 100644 packages/es/jest.setup.js create mode 100644 packages/es/package.json create mode 100644 packages/es/src/base.ts create mode 100644 packages/es/src/config/config.default.ts create mode 100644 packages/es/src/configuration.ts create mode 100644 packages/es/src/decorator/elasticsearch.ts create mode 100644 packages/es/src/elasticsearch.ts create mode 100644 packages/es/src/index.ts create mode 100644 packages/es/src/package.json create mode 100644 packages/es/test/index.test.ts create mode 100644 packages/es/tsconfig.json create mode 100644 packages/file/.editorconfig create mode 100644 packages/file/.eslintrc.json create mode 100644 packages/file/.gitignore create mode 100644 packages/file/.prettierrc.js create mode 100644 packages/file/LICENSE create mode 100644 packages/file/index.d.ts create mode 100644 packages/file/jest.config.js create mode 100644 packages/file/jest.setup.js create mode 100644 packages/file/package.json create mode 100644 packages/file/src/config/config.default.ts create mode 100644 packages/file/src/configuration.ts create mode 100644 packages/file/src/file.ts create mode 100644 packages/file/src/index.ts create mode 100644 packages/file/src/interface.ts create mode 100644 packages/file/src/package.json create mode 100644 packages/file/test/index.test.ts create mode 100644 packages/file/tsconfig.json create mode 100644 packages/iot/.editorconfig create mode 100644 packages/iot/.eslintrc.json create mode 100644 packages/iot/.gitignore create mode 100644 packages/iot/.prettierrc.js create mode 100644 packages/iot/README.md create mode 100644 packages/iot/index.d.ts create mode 100644 packages/iot/jest.config.js create mode 100644 packages/iot/jest.setup.js create mode 100644 packages/iot/package.json create mode 100644 packages/iot/src/config/config.default.ts create mode 100644 packages/iot/src/configuration.ts create mode 100644 packages/iot/src/decorator/mqtt.ts create mode 100644 packages/iot/src/index.ts create mode 100644 packages/iot/src/interface.ts create mode 100644 packages/iot/src/mqtt.ts create mode 100644 packages/iot/src/package.json create mode 100644 packages/iot/test/index.test.ts create mode 100644 packages/iot/tsconfig.json create mode 100644 packages/other/mqemitter-redis/.github/dependabot.yml create mode 100644 packages/other/mqemitter-redis/.github/workflows/ci.yml create mode 100644 packages/other/mqemitter-redis/.gitignore create mode 100644 packages/other/mqemitter-redis/.travis.yml create mode 100644 packages/other/mqemitter-redis/LICENSE create mode 100644 packages/other/mqemitter-redis/README.md create mode 100644 packages/other/mqemitter-redis/mqemitter-redis.js create mode 100644 packages/other/mqemitter-redis/package.json create mode 100644 packages/other/mqemitter-redis/test.js create mode 100644 packages/other/mqemitter-redis/types/index.d.ts create mode 100644 packages/other/mqemitter-redis/types/index.test-d.ts create mode 100644 packages/pay/.editorconfig create mode 100644 packages/pay/.eslintrc.json create mode 100644 packages/pay/.gitignore create mode 100644 packages/pay/.prettierrc.js create mode 100644 packages/pay/index.d.ts create mode 100644 packages/pay/jest.config.js create mode 100644 packages/pay/jest.setup.js create mode 100644 packages/pay/package.json create mode 100644 packages/pay/src/ali.ts create mode 100644 packages/pay/src/config/config.default.ts create mode 100644 packages/pay/src/configuration.ts create mode 100644 packages/pay/src/index.ts create mode 100644 packages/pay/src/interface.ts create mode 100644 packages/pay/src/package.json create mode 100644 packages/pay/src/wx.ts create mode 100644 packages/pay/test/index.test.ts create mode 100644 packages/pay/tsconfig.json create mode 100644 packages/rpc/.editorconfig create mode 100644 packages/rpc/.eslintrc.json create mode 100644 packages/rpc/.gitignore create mode 100644 packages/rpc/.prettierrc.js create mode 100644 packages/rpc/index.d.ts create mode 100644 packages/rpc/jest.config.js create mode 100644 packages/rpc/jest.setup.js create mode 100644 packages/rpc/package.json create mode 100644 packages/rpc/src/config/config.default.ts create mode 100644 packages/rpc/src/configuration.ts create mode 100644 packages/rpc/src/decorator/event/event.ts create mode 100644 packages/rpc/src/decorator/event/handler.ts create mode 100644 packages/rpc/src/decorator/index.ts create mode 100644 packages/rpc/src/decorator/rpc.ts create mode 100644 packages/rpc/src/decorator/transaction.ts create mode 100644 packages/rpc/src/index.ts create mode 100644 packages/rpc/src/package.json create mode 100644 packages/rpc/src/rpc.ts create mode 100644 packages/rpc/src/service/base.ts create mode 100644 packages/rpc/src/test.ts create mode 100644 packages/rpc/src/transaction/event.ts create mode 100644 packages/rpc/test/index.test.ts create mode 100644 packages/rpc/tsconfig.json create mode 100644 packages/task/.editorconfig create mode 100644 packages/task/.eslintrc.json create mode 100644 packages/task/.gitignore create mode 100644 packages/task/.prettierrc.js create mode 100644 packages/task/README.md create mode 100644 packages/task/index.d.ts create mode 100644 packages/task/jest.config.js create mode 100644 packages/task/jest.setup.js create mode 100644 packages/task/package.json create mode 100644 packages/task/src/base.ts create mode 100644 packages/task/src/config/config.default.ts create mode 100644 packages/task/src/configuration.ts create mode 100644 packages/task/src/decorator/queue.ts create mode 100644 packages/task/src/index.ts create mode 100644 packages/task/src/package.json create mode 100644 packages/task/src/queue.ts create mode 100644 packages/task/test/index.test.ts create mode 100644 packages/task/tsconfig.json diff --git a/packages/cloud/.editorconfig b/packages/cloud/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/packages/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/packages/cloud/.eslintrc.json b/packages/cloud/.eslintrc.json new file mode 100644 index 0000000..93e32bd --- /dev/null +++ b/packages/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/packages/cloud/.gitignore b/packages/cloud/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/packages/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/packages/cloud/.prettierrc.js b/packages/cloud/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/packages/cloud/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/packages/cloud/LICENSE b/packages/cloud/LICENSE new file mode 100644 index 0000000..5a739ee --- /dev/null +++ b/packages/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/packages/cloud/index.d.ts b/packages/cloud/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/packages/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/packages/cloud/jest.config.js b/packages/cloud/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/packages/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/packages/cloud/jest.setup.js b/packages/cloud/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/packages/cloud/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/packages/cloud/package.json b/packages/cloud/package.json new file mode 100644 index 0000000..60ff564 --- /dev/null +++ b/packages/cloud/package.json @@ -0,0 +1,47 @@ +{ + "name": "@cool-midway/cloud", + "version": "6.0.0", + "description": "", + "main": "dist/index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "test": "cross-env midway-bin test --ts", + "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/packages/cloud/src/LICENSE b/packages/cloud/src/LICENSE new file mode 100644 index 0000000..5a739ee --- /dev/null +++ b/packages/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/packages/cloud/src/config/config.default.ts b/packages/cloud/src/config/config.default.ts new file mode 100644 index 0000000..03ef2cd --- /dev/null +++ b/packages/cloud/src/config/config.default.ts @@ -0,0 +1,4 @@ +/** + * cool的配置 + */ +export default {}; diff --git a/packages/cloud/src/configuration.ts b/packages/cloud/src/configuration.ts new file mode 100644 index 0000000..ee7447f --- /dev/null +++ b/packages/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/packages/cloud/src/db/index.ts b/packages/cloud/src/db/index.ts new file mode 100644 index 0000000..ae6ec7a --- /dev/null +++ b/packages/cloud/src/db/index.ts @@ -0,0 +1,123 @@ +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, + } + ); + 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/packages/cloud/src/db/source.ts b/packages/cloud/src/db/source.ts new file mode 100644 index 0000000..f5c13af --- /dev/null +++ b/packages/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/packages/cloud/src/func/crud.ts b/packages/cloud/src/func/crud.ts new file mode 100644 index 0000000..842c4cd --- /dev/null +++ b/packages/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/packages/cloud/src/func/index.ts b/packages/cloud/src/func/index.ts new file mode 100644 index 0000000..94da053 --- /dev/null +++ b/packages/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/packages/cloud/src/index.ts b/packages/cloud/src/index.ts new file mode 100644 index 0000000..b95e149 --- /dev/null +++ b/packages/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/packages/cloud/src/interface.ts b/packages/cloud/src/interface.ts new file mode 100644 index 0000000..3c36af0 --- /dev/null +++ b/packages/cloud/src/interface.ts @@ -0,0 +1,11 @@ +/** + * 云函数请求 + */ +export interface CloudReq { + // 云函数名称 + name: string; + // 请求参数 + params: any; + // 调用方法 + method: string; +} diff --git a/packages/cloud/src/package.json b/packages/cloud/src/package.json new file mode 100644 index 0000000..bf99ff5 --- /dev/null +++ b/packages/cloud/src/package.json @@ -0,0 +1,42 @@ +{ + "name": "@cool-midway/cloud", + "version": "6.0.0", + "description": "", + "main": "index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "test": "cross-env midway-bin test --ts", + "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/packages/cloud/src/util.ts b/packages/cloud/src/util.ts new file mode 100644 index 0000000..f98361e --- /dev/null +++ b/packages/cloud/src/util.ts @@ -0,0 +1 @@ +export class CoolCloudUtil {} diff --git a/packages/cloud/test/index.test.ts b/packages/cloud/test/index.test.ts new file mode 100644 index 0000000..a6c75a1 --- /dev/null +++ b/packages/cloud/test/index.test.ts @@ -0,0 +1,14 @@ +import { createLightApp } from '@midwayjs/mock'; +import * as custom from '../src'; + +describe('/test/index.test.ts', () => { + it('test component', async () => { + const app = await createLightApp('', { + imports: [ + custom + ] + }); + const bookService = await app.getApplicationContext().getAsync(custom.BookService); + expect(await bookService.getBookById()).toEqual('hello world'); + }); +}); diff --git a/packages/cloud/tsconfig.json b/packages/cloud/tsconfig.json new file mode 100644 index 0000000..9bb4ab7 --- /dev/null +++ b/packages/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/packages/core/LICENSE b/packages/core/LICENSE new file mode 100644 index 0000000..5a739ee --- /dev/null +++ b/packages/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/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..610c3db --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,3 @@ +# cool-admin + +https://cool-js.com diff --git a/packages/core/_.editorconfig b/packages/core/_.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/packages/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/packages/core/_.eslintrc.json b/packages/core/_.eslintrc.json new file mode 100644 index 0000000..8d20e22 --- /dev/null +++ b/packages/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/packages/core/_.gitignore b/packages/core/_.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/packages/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/packages/core/_.prettierrc.js b/packages/core/_.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/packages/core/_.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/packages/core/index.d.ts b/packages/core/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/packages/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/packages/core/jest.config.js b/packages/core/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/packages/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/packages/core/jest.setup.js b/packages/core/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/packages/core/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..71e32f8 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,56 @@ +{ + "name": "@cool-midway/core", + "version": "6.0.2", + "description": "", + "main": "dist/index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "test": "cross-env midway-bin test --ts", + "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/packages/core/src/LICENSE b/packages/core/src/LICENSE new file mode 100644 index 0000000..5a739ee --- /dev/null +++ b/packages/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/packages/core/src/config/config.default.ts b/packages/core/src/config/config.default.ts new file mode 100644 index 0000000..f93645e --- /dev/null +++ b/packages/core/src/config/config.default.ts @@ -0,0 +1,18 @@ +import { CoolConfig } from "../interface"; + +/** + * cool的配置 + */ +export default { + cool: { + // 是否自动导入数据库 + initDB: false, + // crud配置 + crud: { + // 软删除 + softDelete: true, + // 分页查询每页条数 + pageSize: 15, + }, + } as CoolConfig, +}; diff --git a/packages/core/src/configuration.ts b/packages/core/src/configuration.ts new file mode 100644 index 0000000..4204797 --- /dev/null +++ b/packages/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/packages/core/src/constant/global.ts b/packages/core/src/constant/global.ts new file mode 100644 index 0000000..3cf5a9e --- /dev/null +++ b/packages/core/src/constant/global.ts @@ -0,0 +1,50 @@ +/** + * 返回码 + */ +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", +} diff --git a/packages/core/src/controller/base.ts b/packages/core/src/controller/base.ts new file mode 100644 index 0000000..bb94330 --- /dev/null +++ b/packages/core/src/controller/base.ts @@ -0,0 +1,201 @@ +import { + App, + CONTROLLER_KEY, + getClassMetadata, + Init, + Inject, + Provide, +} from "@midwayjs/decorator"; +import { RESCODE, RESMESSAGE } 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 = { + ...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() { + 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 res = { + code: RESCODE.SUCCESS, + message: RESMESSAGE.SUCCESS, + }; + if (data || data == 0) { + res["data"] = data; + } + return res; + } + + /** + * 失败返回 + * @param message + */ + fail(message?: string, code?: RESCODE) { + return { + code: code ? code : RESCODE.COMMFAIL, + message: message + ? message + : code == RESCODE.VALIDATEFAIL + ? RESMESSAGE.VALIDATEFAIL + : RESMESSAGE.COMMFAIL, + }; + } +} diff --git a/packages/core/src/decorator/cache.ts b/packages/core/src/decorator/cache.ts new file mode 100644 index 0000000..f8195f0 --- /dev/null +++ b/packages/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/packages/core/src/decorator/controller.ts b/packages/core/src/decorator/controller.ts new file mode 100644 index 0000000..98c863b --- /dev/null +++ b/packages/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/packages/core/src/decorator/event.ts b/packages/core/src/decorator/event.ts new file mode 100644 index 0000000..e21b9db --- /dev/null +++ b/packages/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/packages/core/src/decorator/index.ts b/packages/core/src/decorator/index.ts new file mode 100644 index 0000000..05135f8 --- /dev/null +++ b/packages/core/src/decorator/index.ts @@ -0,0 +1,103 @@ +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"; + +/** + * 装饰器 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolDecorator { + @Inject() + typeORMDataSourceManager: TypeORMDataSourceManager; + + @Inject() + decoratorService: MidwayDecoratorService; + + @Inject() + cacheManager: CacheManager; + + @Init() + async init() { + // 事务 + await this.transaction(); + // 缓存 + await this.cache(); + } + + /** + * 缓存 + */ + 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.ttl, + }); + } + 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/packages/core/src/decorator/tag.ts b/packages/core/src/decorator/tag.ts new file mode 100644 index 0000000..69d1357 --- /dev/null +++ b/packages/core/src/decorator/tag.ts @@ -0,0 +1,27 @@ +import { saveClassMetadata, saveModule } from "@midwayjs/decorator"; + +export const COOL_URL_TAG_KEY = "decorator:cool:url: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); + }; +} diff --git a/packages/core/src/decorator/transaction.ts b/packages/core/src/decorator/transaction.ts new file mode 100644 index 0000000..5922dc3 --- /dev/null +++ b/packages/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/packages/core/src/entity/base.ts b/packages/core/src/entity/base.ts new file mode 100644 index 0000000..8a40e4c --- /dev/null +++ b/packages/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/packages/core/src/entity/mongo.ts b/packages/core/src/entity/mongo.ts new file mode 100644 index 0000000..6c80d32 --- /dev/null +++ b/packages/core/src/entity/mongo.ts @@ -0,0 +1,24 @@ +import { + Index, + UpdateDateColumn, + CreateDateColumn, + 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/packages/core/src/entity/typeorm.ts b/packages/core/src/entity/typeorm.ts new file mode 100644 index 0000000..f9c1c8b --- /dev/null +++ b/packages/core/src/entity/typeorm.ts @@ -0,0 +1,3 @@ +import { BaseEntity } from "typeorm"; + +export abstract class CoolBaseEntity extends BaseEntity {} diff --git a/packages/core/src/event/index.ts b/packages/core/src/event/index.ts new file mode 100644 index 0000000..02ae835 --- /dev/null +++ b/packages/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/packages/core/src/exception/base.ts b/packages/core/src/exception/base.ts new file mode 100644 index 0000000..177b46b --- /dev/null +++ b/packages/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/packages/core/src/exception/comm.ts b/packages/core/src/exception/comm.ts new file mode 100644 index 0000000..693a7a9 --- /dev/null +++ b/packages/core/src/exception/comm.ts @@ -0,0 +1,15 @@ +import { RESCODE, RESMESSAGE } from '../constant/global'; +import { BaseException } from './base'; + +/** + * 通用异常 + */ +export class CoolCommException extends BaseException { + constructor(message: string) { + super( + 'CoolCommException', + RESCODE.COMMFAIL, + message ? message : RESMESSAGE.COMMFAIL + ); + } +} diff --git a/packages/core/src/exception/core.ts b/packages/core/src/exception/core.ts new file mode 100644 index 0000000..8c279de --- /dev/null +++ b/packages/core/src/exception/core.ts @@ -0,0 +1,15 @@ +import { RESCODE, RESMESSAGE } from '../constant/global'; +import { BaseException } from './base'; + +/** + * 核心异常 + */ +export class CoolCoreException extends BaseException { + constructor(message: string) { + super( + 'CoolCoreException', + RESCODE.COREFAIL, + message ? message : RESMESSAGE.COREFAIL + ); + } +} diff --git a/packages/core/src/exception/filter.ts b/packages/core/src/exception/filter.ts new file mode 100644 index 0000000..fb894ac --- /dev/null +++ b/packages/core/src/exception/filter.ts @@ -0,0 +1,20 @@ +import { RESCODE } from './../constant/global'; +import { ILogger } from '@midwayjs/core'; +import { Catch, Logger } from '@midwayjs/decorator'; + +/** + * 全局异常处理 + */ +@Catch() +export class CoolExceptionFilter { + @Logger() + coreLogger: ILogger; + + async catch(err) { + this.coreLogger.error(err); + return { + code: err.status || RESCODE.COMMFAIL, + message: err.message, + }; + } +} diff --git a/packages/core/src/exception/validate.ts b/packages/core/src/exception/validate.ts new file mode 100644 index 0000000..6f374e6 --- /dev/null +++ b/packages/core/src/exception/validate.ts @@ -0,0 +1,15 @@ +import { RESCODE, RESMESSAGE } from '../constant/global'; +import { BaseException } from './base'; + +/** + * 校验异常 + */ +export class CoolValidateException extends BaseException { + constructor(message: string) { + super( + 'CoolValidateException', + RESCODE.VALIDATEFAIL, + message ? message : RESMESSAGE.VALIDATEFAIL + ); + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..8f1bbac --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,45 @@ +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 "./interface"; +export * from "./util/func"; +export * from "./constant/global"; diff --git a/packages/core/src/interface.ts b/packages/core/src/interface.ts new file mode 100644 index 0000000..870b2f8 --- /dev/null +++ b/packages/core/src/interface.ts @@ -0,0 +1,300 @@ +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 { + /** 是否自动导入数据库 */ + initDB?: boolean; + // 实体配置 + // entity?: { + // primaryType: "uuid" | "increment" | "rowid" | "identity"; + // }; + /** crud配置 */ + crud?: { + /** 软删除 */ + softDelete: boolean; + /** 分页查询每页条数 */ + pageSize: number; + // 多租户 + // tenant: boolean; + }; + /** elasticsearch配置 */ + es?: { + nodes: string[]; + }; + /** 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; +} + +/** + * 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; +} diff --git a/packages/core/src/module/config.ts b/packages/core/src/module/config.ts new file mode 100644 index 0000000..d3b510a --- /dev/null +++ b/packages/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/packages/core/src/module/import.ts b/packages/core/src/module/import.ts new file mode 100644 index 0000000..0981589 --- /dev/null +++ b/packages/core/src/module/import.ts @@ -0,0 +1,150 @@ +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"; + +/** + * 模块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; + + @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", {}); + }, 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/packages/core/src/package.json b/packages/core/src/package.json new file mode 100644 index 0000000..40d7b6b --- /dev/null +++ b/packages/core/src/package.json @@ -0,0 +1,55 @@ +{ + "name": "@cool-midway/core", + "version": "6.0.2", + "description": "", + "main": "index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "test": "cross-env midway-bin test --ts", + "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/packages/core/src/rest/eps.ts b/packages/core/src/rest/eps.ts new file mode 100644 index 0000000..623b733 --- /dev/null +++ b/packages/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/packages/core/src/service/base.ts b/packages/core/src/service/base.ts new file mode 100644 index 0000000..d3cce2c --- /dev/null +++ b/packages/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.update(param.id, param); + } else { + param.createTime = new Date(); + param.updateTime = new Date(); + await this.entity.insert(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/packages/core/src/tag/data.ts b/packages/core/src/tag/data.ts new file mode 100644 index 0000000..5832e2f --- /dev/null +++ b/packages/core/src/tag/data.ts @@ -0,0 +1,46 @@ +import { CoolUrlTagConfig } from './../decorator/tag'; +import { + CONTROLLER_KEY, + getClassMetadata, + Init, + listModule, + Provide, + Scope, + ScopeEnum, +} from '@midwayjs/decorator'; +import { COOL_URL_TAG_KEY } from '../decorator/tag'; + +/** + * URL标签 + */ +@Provide() +@Scope(ScopeEnum.Singleton) +export class CoolUrlTagData { + data = {}; + @Init() + async init() { + const tags = listModule(COOL_URL_TAG_KEY); + for (const controller of tags) { + const controllerOption = getClassMetadata(CONTROLLER_KEY, controller); + const tagOption: CoolUrlTagConfig = getClassMetadata( + COOL_URL_TAG_KEY, + controller + ); + const data: string[] = this.data[tagOption.key] || []; + this.data[tagOption.key] = data.concat( + tagOption.value.map(e => { + return controllerOption.prefix + '/' + e; + }) + ); + } + } + + /** + * 根据键获得 + * @param key + * @returns + */ + byKey(key: string): string[] { + return this.data[key]; + } +} diff --git a/packages/core/src/util/func.ts b/packages/core/src/util/func.ts new file mode 100644 index 0000000..f796bbc --- /dev/null +++ b/packages/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/packages/core/src/util/location.ts b/packages/core/src/util/location.ts new file mode 100644 index 0000000..5558180 --- /dev/null +++ b/packages/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/packages/core/test/index.test.ts b/packages/core/test/index.test.ts new file mode 100644 index 0000000..a6c75a1 --- /dev/null +++ b/packages/core/test/index.test.ts @@ -0,0 +1,14 @@ +import { createLightApp } from '@midwayjs/mock'; +import * as custom from '../src'; + +describe('/test/index.test.ts', () => { + it('test component', async () => { + const app = await createLightApp('', { + imports: [ + custom + ] + }); + const bookService = await app.getApplicationContext().getAsync(custom.BookService); + expect(await bookService.getBookById()).toEqual('hello world'); + }); +}); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..9bb4ab7 --- /dev/null +++ b/packages/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/packages/es/.editorconfig b/packages/es/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/packages/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/packages/es/.eslintrc.json b/packages/es/.eslintrc.json new file mode 100644 index 0000000..aad9755 --- /dev/null +++ b/packages/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/packages/es/.gitignore b/packages/es/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/packages/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/packages/es/.prettierrc.js b/packages/es/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/packages/es/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/packages/es/README.md b/packages/es/README.md new file mode 100644 index 0000000..610c3db --- /dev/null +++ b/packages/es/README.md @@ -0,0 +1,3 @@ +# cool-admin + +https://cool-js.com diff --git a/packages/es/index.d.ts b/packages/es/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/packages/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/packages/es/jest.config.js b/packages/es/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/packages/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/packages/es/jest.setup.js b/packages/es/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/packages/es/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/packages/es/package.json b/packages/es/package.json new file mode 100644 index 0000000..9a5dd36 --- /dev/null +++ b/packages/es/package.json @@ -0,0 +1,44 @@ +{ + "name": "@cool-midway/es", + "version": "6.0.0", + "description": "cool-js.com elasticsearch", + "main": "dist/index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "test": "cross-env midway-bin test --ts", + "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/packages/es/src/base.ts b/packages/es/src/base.ts new file mode 100644 index 0000000..b975ddb --- /dev/null +++ b/packages/es/src/base.ts @@ -0,0 +1,569 @@ +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; + } + + /** + * 查询 + * @param body + */ + async find(body?: any, size?: number) { + if (!body) { + body = {}; + } + 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 (!size) { + size = 20; + } + if (!body) { + body = {}; + } + const total = await this.findCount(body); + body.from = (page - 1) * size; + body.size = 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/packages/es/src/config/config.default.ts b/packages/es/src/config/config.default.ts new file mode 100644 index 0000000..d8324ca --- /dev/null +++ b/packages/es/src/config/config.default.ts @@ -0,0 +1,4 @@ +export const customKey = { + a: 1, + b: 'hello', +}; diff --git a/packages/es/src/configuration.ts b/packages/es/src/configuration.ts new file mode 100644 index 0000000..93d6842 --- /dev/null +++ b/packages/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/packages/es/src/decorator/elasticsearch.ts b/packages/es/src/decorator/elasticsearch.ts new file mode 100644 index 0000000..7c89d0f --- /dev/null +++ b/packages/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/packages/es/src/elasticsearch.ts b/packages/es/src/elasticsearch.ts new file mode 100644 index 0000000..6bb32e6 --- /dev/null +++ b/packages/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] }); + } else { + this.client = new Client({ nodes: this.esConfig.nodes }); + } + 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/packages/es/src/index.ts b/packages/es/src/index.ts new file mode 100644 index 0000000..438e1a9 --- /dev/null +++ b/packages/es/src/index.ts @@ -0,0 +1,15 @@ +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[]; +} diff --git a/packages/es/src/package.json b/packages/es/src/package.json new file mode 100644 index 0000000..c599b18 --- /dev/null +++ b/packages/es/src/package.json @@ -0,0 +1,42 @@ +{ + "name": "@cool-midway/es", + "version": "6.0.0", + "description": "cool-js.com elasticsearch", + "main": "index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "test": "cross-env midway-bin test --ts", + "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/packages/es/test/index.test.ts b/packages/es/test/index.test.ts new file mode 100644 index 0000000..a6c75a1 --- /dev/null +++ b/packages/es/test/index.test.ts @@ -0,0 +1,14 @@ +import { createLightApp } from '@midwayjs/mock'; +import * as custom from '../src'; + +describe('/test/index.test.ts', () => { + it('test component', async () => { + const app = await createLightApp('', { + imports: [ + custom + ] + }); + const bookService = await app.getApplicationContext().getAsync(custom.BookService); + expect(await bookService.getBookById()).toEqual('hello world'); + }); +}); diff --git a/packages/es/tsconfig.json b/packages/es/tsconfig.json new file mode 100644 index 0000000..f01e1d2 --- /dev/null +++ b/packages/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/packages/file/.editorconfig b/packages/file/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/packages/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/packages/file/.eslintrc.json b/packages/file/.eslintrc.json new file mode 100644 index 0000000..93e32bd --- /dev/null +++ b/packages/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/packages/file/.gitignore b/packages/file/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/packages/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/packages/file/.prettierrc.js b/packages/file/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/packages/file/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/packages/file/LICENSE b/packages/file/LICENSE new file mode 100644 index 0000000..5a739ee --- /dev/null +++ b/packages/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/packages/file/index.d.ts b/packages/file/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/packages/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/packages/file/jest.config.js b/packages/file/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/packages/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/packages/file/jest.setup.js b/packages/file/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/packages/file/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/packages/file/package.json b/packages/file/package.json new file mode 100644 index 0000000..c1f1778 --- /dev/null +++ b/packages/file/package.json @@ -0,0 +1,53 @@ +{ + "name": "@cool-midway/file", + "version": "6.0.1", + "description": "", + "main": "dist/index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "test": "cross-env midway-bin test --ts", + "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/packages/file/src/config/config.default.ts b/packages/file/src/config/config.default.ts new file mode 100644 index 0000000..9b0cf96 --- /dev/null +++ b/packages/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/packages/file/src/configuration.ts b/packages/file/src/configuration.ts new file mode 100644 index 0000000..e4ef4ba --- /dev/null +++ b/packages/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/packages/file/src/file.ts b/packages/file/src/file.ts new file mode 100644 index 0000000..2bfc5bf --- /dev/null +++ b/packages/file/src/file.ts @@ -0,0 +1,475 @@ +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 + '/' + name); + } + } + ); + }); + } + } + } + + /** + * 上传文件 + * @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, + } = this.config.oss; + const oss = { + bucket, + region: endpoint.split('.')[0], // 我的是 hangzhou + accessKeyId, + accessKeySecret, + expAfter, // 签名失效时间,毫秒 + maxSize, // 文件最大的 size + }; + const 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, + 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/packages/file/src/index.ts b/packages/file/src/index.ts new file mode 100644 index 0000000..5a58056 --- /dev/null +++ b/packages/file/src/index.ts @@ -0,0 +1,5 @@ +export { CoolFileConfiguration as Configuration } from './configuration'; + +export * from './interface'; + +export * from './file'; diff --git a/packages/file/src/interface.ts b/packages/file/src/interface.ts new file mode 100644 index 0000000..1871a29 --- /dev/null +++ b/packages/file/src/interface.ts @@ -0,0 +1,103 @@ +// 模式 +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; +} + +/** + * 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/packages/file/src/package.json b/packages/file/src/package.json new file mode 100644 index 0000000..08f66e4 --- /dev/null +++ b/packages/file/src/package.json @@ -0,0 +1,53 @@ +{ + "name": "@cool-midway/file", + "version": "6.0.1", + "description": "", + "main": "index.js", + "typings": "index.d.ts", + "scripts": { + "build": "cross-env midway-bin build -c", + "test": "cross-env midway-bin test --ts", + "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/packages/file/test/index.test.ts b/packages/file/test/index.test.ts new file mode 100644 index 0000000..a6c75a1 --- /dev/null +++ b/packages/file/test/index.test.ts @@ -0,0 +1,14 @@ +import { createLightApp } from '@midwayjs/mock'; +import * as custom from '../src'; + +describe('/test/index.test.ts', () => { + it('test component', async () => { + const app = await createLightApp('', { + imports: [ + custom + ] + }); + const bookService = await app.getApplicationContext().getAsync(custom.BookService); + expect(await bookService.getBookById()).toEqual('hello world'); + }); +}); diff --git a/packages/file/tsconfig.json b/packages/file/tsconfig.json new file mode 100644 index 0000000..9bb4ab7 --- /dev/null +++ b/packages/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/packages/iot/.editorconfig b/packages/iot/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/packages/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/packages/iot/.eslintrc.json b/packages/iot/.eslintrc.json new file mode 100644 index 0000000..aad9755 --- /dev/null +++ b/packages/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/packages/iot/.gitignore b/packages/iot/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/packages/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/packages/iot/.prettierrc.js b/packages/iot/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/packages/iot/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/packages/iot/README.md b/packages/iot/README.md new file mode 100644 index 0000000..610c3db --- /dev/null +++ b/packages/iot/README.md @@ -0,0 +1,3 @@ +# cool-admin + +https://cool-js.com diff --git a/packages/iot/index.d.ts b/packages/iot/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/packages/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/packages/iot/jest.config.js b/packages/iot/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/packages/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/packages/iot/jest.setup.js b/packages/iot/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/packages/iot/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/packages/iot/package.json b/packages/iot/package.json new file mode 100644 index 0000000..299377c --- /dev/null +++ b/packages/iot/package.json @@ -0,0 +1,53 @@ +{ + "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", + "test": "cross-env midway-bin test --ts", + "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/packages/iot/src/config/config.default.ts b/packages/iot/src/config/config.default.ts new file mode 100644 index 0000000..ee625da --- /dev/null +++ b/packages/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/packages/iot/src/configuration.ts b/packages/iot/src/configuration.ts new file mode 100644 index 0000000..ecb17aa --- /dev/null +++ b/packages/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/packages/iot/src/decorator/mqtt.ts b/packages/iot/src/decorator/mqtt.ts new file mode 100644 index 0000000..8feb4f6 --- /dev/null +++ b/packages/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/packages/iot/src/index.ts b/packages/iot/src/index.ts new file mode 100644 index 0000000..e827cde --- /dev/null +++ b/packages/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/packages/iot/src/interface.ts b/packages/iot/src/interface.ts new file mode 100644 index 0000000..ca033f1 --- /dev/null +++ b/packages/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/packages/iot/src/mqtt.ts b/packages/iot/src/mqtt.ts new file mode 100644 index 0000000..7fade44 --- /dev/null +++ b/packages/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/packages/iot/src/package.json b/packages/iot/src/package.json new file mode 100644 index 0000000..5c9fb5b --- /dev/null +++ b/packages/iot/src/package.json @@ -0,0 +1,53 @@ +{ + "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", + "test": "cross-env midway-bin test --ts", + "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/packages/iot/test/index.test.ts b/packages/iot/test/index.test.ts new file mode 100644 index 0000000..a6c75a1 --- /dev/null +++ b/packages/iot/test/index.test.ts @@ -0,0 +1,14 @@ +import { createLightApp } from '@midwayjs/mock'; +import * as custom from '../src'; + +describe('/test/index.test.ts', () => { + it('test component', async () => { + const app = await createLightApp('', { + imports: [ + custom + ] + }); + const bookService = await app.getApplicationContext().getAsync(custom.BookService); + expect(await bookService.getBookById()).toEqual('hello world'); + }); +}); diff --git a/packages/iot/tsconfig.json b/packages/iot/tsconfig.json new file mode 100644 index 0000000..f01e1d2 --- /dev/null +++ b/packages/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/packages/other/mqemitter-redis/.github/dependabot.yml b/packages/other/mqemitter-redis/.github/dependabot.yml new file mode 100644 index 0000000..4872c5a --- /dev/null +++ b/packages/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/packages/other/mqemitter-redis/.github/workflows/ci.yml b/packages/other/mqemitter-redis/.github/workflows/ci.yml new file mode 100644 index 0000000..c9b1558 --- /dev/null +++ b/packages/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/packages/other/mqemitter-redis/.gitignore b/packages/other/mqemitter-redis/.gitignore new file mode 100644 index 0000000..6c655ef --- /dev/null +++ b/packages/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/packages/other/mqemitter-redis/.travis.yml b/packages/other/mqemitter-redis/.travis.yml new file mode 100644 index 0000000..73469a0 --- /dev/null +++ b/packages/other/mqemitter-redis/.travis.yml @@ -0,0 +1,9 @@ +language: node_js +node_js: + - "10" + - "12" + - "13" + - "14" + - "15" +services: + - redis diff --git a/packages/other/mqemitter-redis/LICENSE b/packages/other/mqemitter-redis/LICENSE new file mode 100644 index 0000000..feb2ed0 --- /dev/null +++ b/packages/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/packages/other/mqemitter-redis/README.md b/packages/other/mqemitter-redis/README.md new file mode 100644 index 0000000..e4cd108 --- /dev/null +++ b/packages/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/packages/other/mqemitter-redis/mqemitter-redis.js b/packages/other/mqemitter-redis/mqemitter-redis.js new file mode 100644 index 0000000..62f2222 --- /dev/null +++ b/packages/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/packages/other/mqemitter-redis/package.json b/packages/other/mqemitter-redis/package.json new file mode 100644 index 0000000..f2495f6 --- /dev/null +++ b/packages/other/mqemitter-redis/package.json @@ -0,0 +1,46 @@ +{ + "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": { + "test:types": "tsd", + "test": "standard && tape test.js | faucet && tsd" + }, + "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/packages/other/mqemitter-redis/test.js b/packages/other/mqemitter-redis/test.js new file mode 100644 index 0000000..8305b3a --- /dev/null +++ b/packages/other/mqemitter-redis/test.js @@ -0,0 +1,116 @@ +'use strict' + +const redis = require('./') +const test = require('tape').test +const abstractTests = require('mqemitter/abstractTest.js') + +abstractTests({ + builder: redis, + test +}) + +abstractTests({ + builder: function (opts) { return new redis.MQEmitterRedisPrefix('some_prefix/', opts) }, + test +}) + +function noop () {} + +test('actual unsubscribe from Redis', function (t) { + const e = redis() + + e.subConn.on('message', function (topic, message) { + t.fail('the message should not be emitted') + }) + + e.on('hello', noop) + e.removeListener('hello', noop) + e.emit({ topic: 'hello' }, function (err) { + t.notOk(err) + e.close(function () { + t.end() + }) + }) +}) + +test('ioredis connect event', function (t) { + const e = redis() + + let subConnectEventReceived = false + let pubConnectEventReceived = false + + e.state.on('pubConnect', function () { + pubConnectEventReceived = true + newConnectionEvent() + }) + + e.state.on('subConnect', function () { + subConnectEventReceived = true + newConnectionEvent() + }) + + function newConnectionEvent () { + if (subConnectEventReceived && pubConnectEventReceived) { + e.close(function () { + t.end() + }) + } + } +}) + +test('ioredis error event', function (t) { + const e = redis({ host: '127' }) + + t.plan(1) + + e.state.once('error', function (err) { + t.deepEqual(err.message.substr(0, 7), 'connect') + e.close(function () { + t.end() + }) + }) +}) + +test('topic pattern adapter', function (t) { + const e = redis() + + const mqttTopic = 'rooms/+/devices/+/status' + const expectedRedisPattern = 'rooms/*/devices/*/status' + + const subTopic = e._subTopic(mqttTopic) + + t.plan(1) + + t.deepEqual(subTopic, expectedRedisPattern) + + e.close(function () { + t.end() + }) +}) + +test('ioredis connection string', function (t) { + const e = redis({ + connectionString: 'redis://localhost:6379/0' + }) + + let subConnectEventReceived = false + let pubConnectEventReceived = false + + e.state.on('pubConnect', function () { + pubConnectEventReceived = true + newConnectionEvent() + }) + + e.state.on('subConnect', function () { + subConnectEventReceived = true + newConnectionEvent() + }) + + function newConnectionEvent () { + if (subConnectEventReceived && pubConnectEventReceived) { + e.close(function () { + t.end() + }) + } + } +}) diff --git a/packages/other/mqemitter-redis/types/index.d.ts b/packages/other/mqemitter-redis/types/index.d.ts new file mode 100644 index 0000000..a6a5a6c --- /dev/null +++ b/packages/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/packages/other/mqemitter-redis/types/index.test-d.ts b/packages/other/mqemitter-redis/types/index.test-d.ts new file mode 100644 index 0000000..f9d83d2 --- /dev/null +++ b/packages/other/mqemitter-redis/types/index.test-d.ts @@ -0,0 +1,42 @@ +import { expectError, expectType } from 'tsd'; +import mqEmitterRedis, { Message, MQEmitterRedis } from '.'; + +expectType(mqEmitterRedis()); + +expectType( + mqEmitterRedis({ concurrency: 200, matchEmptyLevels: true }) +); + +expectType( + mqEmitterRedis({ + concurrency: 10, + matchEmptyLevels: true, + separator: '/', + wildcardOne: '+', + wildcardSome: '#', + connectionString: 'redis://:authpassword@127.0.0.1:6380/4', + }) +); + +expectType( + mqEmitterRedis({ + concurrency: 10, + matchEmptyLevels: true, + host: 'localhost', + port: 6379, + reconnectOnError: (error: Error) => true, + retryStrategy: (times: number) => times * 1.5, + }) +); + +function listener(message: Message, done: () => void) {} + +expectType(mqEmitterRedis().on('topic', listener)); + +expectType(mqEmitterRedis().removeListener('topic', listener)); + +expectError(mqEmitterRedis().emit(null)); + +expectType(mqEmitterRedis().emit({ topic: 'test', prop1: 'prop1' })); + +expectType(mqEmitterRedis().close(() => null)); diff --git a/packages/pay/.editorconfig b/packages/pay/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/packages/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/packages/pay/.eslintrc.json b/packages/pay/.eslintrc.json new file mode 100644 index 0000000..aad9755 --- /dev/null +++ b/packages/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/packages/pay/.gitignore b/packages/pay/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/packages/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/packages/pay/.prettierrc.js b/packages/pay/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/packages/pay/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/packages/pay/index.d.ts b/packages/pay/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/packages/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/packages/pay/jest.config.js b/packages/pay/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/packages/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/packages/pay/jest.setup.js b/packages/pay/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/packages/pay/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/packages/pay/package.json b/packages/pay/package.json new file mode 100644 index 0000000..f80982a --- /dev/null +++ b/packages/pay/package.json @@ -0,0 +1,50 @@ +{ + "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", + "test": "cross-env midway-bin test --ts", + "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/packages/pay/src/ali.ts b/packages/pay/src/ali.ts new file mode 100644 index 0000000..b46e992 --- /dev/null +++ b/packages/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/packages/pay/src/config/config.default.ts b/packages/pay/src/config/config.default.ts new file mode 100644 index 0000000..d8324ca --- /dev/null +++ b/packages/pay/src/config/config.default.ts @@ -0,0 +1,4 @@ +export const customKey = { + a: 1, + b: 'hello', +}; diff --git a/packages/pay/src/configuration.ts b/packages/pay/src/configuration.ts new file mode 100644 index 0000000..a77d2e5 --- /dev/null +++ b/packages/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/packages/pay/src/index.ts b/packages/pay/src/index.ts new file mode 100644 index 0000000..c040890 --- /dev/null +++ b/packages/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/packages/pay/src/interface.ts b/packages/pay/src/interface.ts new file mode 100644 index 0000000..43eba55 --- /dev/null +++ b/packages/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/packages/pay/src/package.json b/packages/pay/src/package.json new file mode 100644 index 0000000..f679a33 --- /dev/null +++ b/packages/pay/src/package.json @@ -0,0 +1,50 @@ +{ + "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", + "test": "cross-env midway-bin test --ts", + "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/packages/pay/src/wx.ts b/packages/pay/src/wx.ts new file mode 100644 index 0000000..09c67fc --- /dev/null +++ b/packages/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/packages/pay/test/index.test.ts b/packages/pay/test/index.test.ts new file mode 100644 index 0000000..a6c75a1 --- /dev/null +++ b/packages/pay/test/index.test.ts @@ -0,0 +1,14 @@ +import { createLightApp } from '@midwayjs/mock'; +import * as custom from '../src'; + +describe('/test/index.test.ts', () => { + it('test component', async () => { + const app = await createLightApp('', { + imports: [ + custom + ] + }); + const bookService = await app.getApplicationContext().getAsync(custom.BookService); + expect(await bookService.getBookById()).toEqual('hello world'); + }); +}); diff --git a/packages/pay/tsconfig.json b/packages/pay/tsconfig.json new file mode 100644 index 0000000..f01e1d2 --- /dev/null +++ b/packages/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/packages/rpc/.editorconfig b/packages/rpc/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/packages/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/packages/rpc/.eslintrc.json b/packages/rpc/.eslintrc.json new file mode 100644 index 0000000..0abd65a --- /dev/null +++ b/packages/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/packages/rpc/.gitignore b/packages/rpc/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/packages/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/packages/rpc/.prettierrc.js b/packages/rpc/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/packages/rpc/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/packages/rpc/index.d.ts b/packages/rpc/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/packages/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/packages/rpc/jest.config.js b/packages/rpc/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/packages/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/packages/rpc/jest.setup.js b/packages/rpc/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/packages/rpc/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/packages/rpc/package.json b/packages/rpc/package.json new file mode 100644 index 0000000..0b42cac --- /dev/null +++ b/packages/rpc/package.json @@ -0,0 +1,48 @@ +{ + "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", + "test": "cross-env midway-bin test --ts", + "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/packages/rpc/src/config/config.default.ts b/packages/rpc/src/config/config.default.ts new file mode 100644 index 0000000..a30c406 --- /dev/null +++ b/packages/rpc/src/config/config.default.ts @@ -0,0 +1,6 @@ +/** + * cool的配置 + */ +export default { + cool: {}, +}; diff --git a/packages/rpc/src/configuration.ts b/packages/rpc/src/configuration.ts new file mode 100644 index 0000000..16ae071 --- /dev/null +++ b/packages/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/packages/rpc/src/decorator/event/event.ts b/packages/rpc/src/decorator/event/event.ts new file mode 100644 index 0000000..526e11e --- /dev/null +++ b/packages/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/packages/rpc/src/decorator/event/handler.ts b/packages/rpc/src/decorator/event/handler.ts new file mode 100644 index 0000000..e5a8328 --- /dev/null +++ b/packages/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/packages/rpc/src/decorator/index.ts b/packages/rpc/src/decorator/index.ts new file mode 100644 index 0000000..3299815 --- /dev/null +++ b/packages/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/packages/rpc/src/decorator/rpc.ts b/packages/rpc/src/decorator/rpc.ts new file mode 100644 index 0000000..8e91b4b --- /dev/null +++ b/packages/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/packages/rpc/src/decorator/transaction.ts b/packages/rpc/src/decorator/transaction.ts new file mode 100644 index 0000000..93cc5ca --- /dev/null +++ b/packages/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/packages/rpc/src/index.ts b/packages/rpc/src/index.ts new file mode 100644 index 0000000..4d68d39 --- /dev/null +++ b/packages/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/packages/rpc/src/package.json b/packages/rpc/src/package.json new file mode 100644 index 0000000..0d762c0 --- /dev/null +++ b/packages/rpc/src/package.json @@ -0,0 +1,48 @@ +{ + "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", + "test": "cross-env midway-bin test --ts", + "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/packages/rpc/src/rpc.ts b/packages/rpc/src/rpc.ts new file mode 100644 index 0000000..b6ecfd2 --- /dev/null +++ b/packages/rpc/src/rpc.ts @@ -0,0 +1,274 @@ +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 { 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(_.lowerFirst(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/packages/rpc/src/service/base.ts b/packages/rpc/src/service/base.ts new file mode 100644 index 0000000..8d0abf3 --- /dev/null +++ b/packages/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/packages/rpc/src/test.ts b/packages/rpc/src/test.ts new file mode 100644 index 0000000..d15b931 --- /dev/null +++ b/packages/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/packages/rpc/src/transaction/event.ts b/packages/rpc/src/transaction/event.ts new file mode 100644 index 0000000..c81cf6c --- /dev/null +++ b/packages/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/packages/rpc/test/index.test.ts b/packages/rpc/test/index.test.ts new file mode 100644 index 0000000..a6c75a1 --- /dev/null +++ b/packages/rpc/test/index.test.ts @@ -0,0 +1,14 @@ +import { createLightApp } from '@midwayjs/mock'; +import * as custom from '../src'; + +describe('/test/index.test.ts', () => { + it('test component', async () => { + const app = await createLightApp('', { + imports: [ + custom + ] + }); + const bookService = await app.getApplicationContext().getAsync(custom.BookService); + expect(await bookService.getBookById()).toEqual('hello world'); + }); +}); diff --git a/packages/rpc/tsconfig.json b/packages/rpc/tsconfig.json new file mode 100644 index 0000000..f01e1d2 --- /dev/null +++ b/packages/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/packages/task/.editorconfig b/packages/task/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/packages/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/packages/task/.eslintrc.json b/packages/task/.eslintrc.json new file mode 100644 index 0000000..fa5dee4 --- /dev/null +++ b/packages/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/packages/task/.gitignore b/packages/task/.gitignore new file mode 100644 index 0000000..13bc3a6 --- /dev/null +++ b/packages/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/packages/task/.prettierrc.js b/packages/task/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/packages/task/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/packages/task/README.md b/packages/task/README.md new file mode 100644 index 0000000..610c3db --- /dev/null +++ b/packages/task/README.md @@ -0,0 +1,3 @@ +# cool-admin + +https://cool-js.com diff --git a/packages/task/index.d.ts b/packages/task/index.d.ts new file mode 100644 index 0000000..e0065de --- /dev/null +++ b/packages/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/packages/task/jest.config.js b/packages/task/jest.config.js new file mode 100644 index 0000000..82a8b85 --- /dev/null +++ b/packages/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/packages/task/jest.setup.js b/packages/task/jest.setup.js new file mode 100644 index 0000000..1399c91 --- /dev/null +++ b/packages/task/jest.setup.js @@ -0,0 +1 @@ +jest.setTimeout(30000); diff --git a/packages/task/package.json b/packages/task/package.json new file mode 100644 index 0000000..a98f696 --- /dev/null +++ b/packages/task/package.json @@ -0,0 +1,51 @@ +{ + "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", + "test": "cross-env midway-bin test --ts", + "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/packages/task/src/base.ts b/packages/task/src/base.ts new file mode 100644 index 0000000..8d1e5b6 --- /dev/null +++ b/packages/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/packages/task/src/config/config.default.ts b/packages/task/src/config/config.default.ts new file mode 100644 index 0000000..a30c406 --- /dev/null +++ b/packages/task/src/config/config.default.ts @@ -0,0 +1,6 @@ +/** + * cool的配置 + */ +export default { + cool: {}, +}; diff --git a/packages/task/src/configuration.ts b/packages/task/src/configuration.ts new file mode 100644 index 0000000..4baebdf --- /dev/null +++ b/packages/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/packages/task/src/decorator/queue.ts b/packages/task/src/decorator/queue.ts new file mode 100644 index 0000000..6303fb4 --- /dev/null +++ b/packages/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/packages/task/src/index.ts b/packages/task/src/index.ts new file mode 100644 index 0000000..37134d7 --- /dev/null +++ b/packages/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/packages/task/src/package.json b/packages/task/src/package.json new file mode 100644 index 0000000..5888897 --- /dev/null +++ b/packages/task/src/package.json @@ -0,0 +1,51 @@ +{ + "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", + "test": "cross-env midway-bin test --ts", + "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/packages/task/src/queue.ts b/packages/task/src/queue.ts new file mode 100644 index 0000000..533a824 --- /dev/null +++ b/packages/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/packages/task/test/index.test.ts b/packages/task/test/index.test.ts new file mode 100644 index 0000000..a6c75a1 --- /dev/null +++ b/packages/task/test/index.test.ts @@ -0,0 +1,14 @@ +import { createLightApp } from '@midwayjs/mock'; +import * as custom from '../src'; + +describe('/test/index.test.ts', () => { + it('test component', async () => { + const app = await createLightApp('', { + imports: [ + custom + ] + }); + const bookService = await app.getApplicationContext().getAsync(custom.BookService); + expect(await bookService.getBookById()).toEqual('hello world'); + }); +}); diff --git a/packages/task/tsconfig.json b/packages/task/tsconfig.json new file mode 100644 index 0000000..f01e1d2 --- /dev/null +++ b/packages/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" + ] +}