diff --git a/.env.example b/.env.example index b8878ebe0..d70efc28a 100644 --- a/.env.example +++ b/.env.example @@ -51,7 +51,7 @@ INIT_ROOT_USERNAME=nocobase DEFAULT_STORAGE_TYPE=local # LOCAL STORAGE -LOCAL_STORAGE_BASE_URL= +LOCAL_STORAGE_BASE_URL=/storage/uploads LOCAL_STORAGE_DEST=storage/uploads # ALI OSS STORAGE diff --git a/.github/workflows/nocobase-test-backend.yml b/.github/workflows/nocobase-test-backend.yml index bcdc79377..7cc50bdc0 100644 --- a/.github/workflows/nocobase-test-backend.yml +++ b/.github/workflows/nocobase-test-backend.yml @@ -44,7 +44,7 @@ jobs: DB_DIALECT: sqlite DB_STORAGE: /tmp/db.sqlite DB_UNDERSCORED: ${{ matrix.underscored }} - timeout-minutes: 35 + timeout-minutes: 40 postgres-test: strategy: @@ -92,7 +92,7 @@ jobs: DB_UNDERSCORED: ${{ matrix.underscored }} DB_SCHEMA: ${{ matrix.schema }} COLLECTION_MANAGER_SCHEMA: ${{ matrix.collection_schema }} - timeout-minutes: 35 + timeout-minutes: 40 mysql-test: strategy: @@ -128,4 +128,4 @@ jobs: DB_PASSWORD: password DB_DATABASE: nocobase DB_UNDERSCORED: ${{ matrix.underscored }} - timeout-minutes: 35 + timeout-minutes: 40 diff --git a/Dockerfile b/Dockerfile index ad2fb1e0d..f9c04f42b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,6 @@ RUN cd /app \ FROM node:16.20-bullseye-slim RUN apt-get update && apt-get install -y nginx - RUN rm -rf /etc/nginx/sites-enabled/default COPY ./docker/nocobase/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz diff --git a/README.md b/README.md index b537c0e3f..b731a6b2c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ NocoBase is in early stage of development and is subject to frequent changes, pl ## Recent major updates +- [v0.13: New application status flow - 2023/08/24](https://docs.nocobase.com/welcome/release/v13-changelog) - [v0.12: New plugin build tool - 2023/08/01](https://docs.nocobase.com/welcome/release/v12-changelog) - [v0.11: New client application, plugin and router - 2023/07/08](http://docs.nocobase.com/welcome/release/v11-changelog) - [v0.10: Update instructions - 2023/06/23](http://docs.nocobase.com/welcome/release/v10-changelog) diff --git a/README.tr-TR.md b/README.tr-TR.md index cf152eb87..2d9b3751f 100644 --- a/README.tr-TR.md +++ b/README.tr-TR.md @@ -10,6 +10,8 @@ NocoBase v0.10 has been released, see [v0.10: update instructions](http://docs.n ## Recent major updates +- [v0.13: New application status flow - 2023/08/24](https://docs.nocobase.com/welcome/release/v13-changelog) +- [v0.12: New plugin build tool - 2023/08/01](https://docs.nocobase.com/welcome/release/v12-changelog) - [v0.11: New client application, plugin and router - 2023/07/08](http://docs.nocobase.com/welcome/release/v11-changelog) - [v0.10: Update instructions - 2023/06/23](http://docs.nocobase.com/welcome/release/v10-changelog) diff --git a/README.zh-CN.md b/README.zh-CN.md index b503767c1..a595adb22 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,6 +8,7 @@ NocoBase 正处在早期开发阶段,可能变动频繁,请谨慎用于生 ## 最近重要更新 +- [v0.13: 全新的应用状态流转 - 2023/08/24](https://docs-cn.nocobase.com/welcome/release/v13-changelog) - [v0.12: 全新的插件构建工具 - 2023/08/01](https://docs-cn.nocobase.com/welcome/release/v12-changelog) - [v0.11: 全新的客户端 Application、Plugin 和 Router - 2023/07/08](https://docs-cn.nocobase.com/welcome/release/v11-changelog) - [v0.10: Update instructions - 2023/06/23](https://docs-cn.nocobase.com/welcome/release/v10-changelog) diff --git a/docker/nocobase-full/nocobase.conf b/docker/nocobase-full/nocobase.conf index 57d5ae427..fa37c8ea8 100644 --- a/docker/nocobase-full/nocobase.conf +++ b/docker/nocobase-full/nocobase.conf @@ -1,3 +1,17 @@ +log_format apm '"$time_local" client=$remote_addr ' + 'method=$request_method request="$request" ' + 'request_length=$request_length ' + 'status=$status bytes_sent=$bytes_sent ' + 'body_bytes_sent=$body_bytes_sent ' + 'referer=$http_referer ' + 'user_agent="$http_user_agent" ' + 'upstream_addr=$upstream_addr ' + 'upstream_status=$upstream_status ' + 'request_time=$request_time ' + 'upstream_response_time=$upstream_response_time ' + 'upstream_connect_time=$upstream_connect_time ' + 'upstream_header_time=$upstream_header_time'; + server { listen 80; server_name _; @@ -5,6 +19,8 @@ server { index index.html; client_max_body_size 20M; + access_log /var/log/nginx/nocobase.log apm; + location /storage/uploads/ { alias /app/nocobase/storage/uploads/; autoindex off; diff --git a/docker/nocobase/nocobase.conf b/docker/nocobase/nocobase.conf index fc5d3fc2a..695dce29e 100644 --- a/docker/nocobase/nocobase.conf +++ b/docker/nocobase/nocobase.conf @@ -1,9 +1,24 @@ +log_format apm '"$time_local" client=$remote_addr ' + 'method=$request_method request="$request" ' + 'request_length=$request_length ' + 'status=$status bytes_sent=$bytes_sent ' + 'body_bytes_sent=$body_bytes_sent ' + 'referer=$http_referer ' + 'user_agent="$http_user_agent" ' + 'upstream_addr=$upstream_addr ' + 'upstream_status=$upstream_status ' + 'request_time=$request_time ' + 'upstream_response_time=$upstream_response_time ' + 'upstream_connect_time=$upstream_connect_time ' + 'upstream_header_time=$upstream_header_time'; + server { listen 80; server_name _; root /app/nocobase/node_modules/@nocobase/app/dist/client; index index.html; client_max_body_size 20M; + access_log /var/log/nginx/nocobase.log apm; gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; @@ -50,4 +65,12 @@ server { proxy_read_timeout 600; send_timeout 600; } + + location /ws { + proxy_pass http://127.0.0.1:13000/ws; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } } diff --git a/docs/config.ts b/docs/config.ts index e069be89b..fde014e6b 100644 --- a/docs/config.ts +++ b/docs/config.ts @@ -84,6 +84,7 @@ const sidebar = { }, // '/welcome/release/index', // '/welcome/release/v08-changelog', + '/welcome/release/v13-changelog', '/welcome/release/v12-changelog', '/welcome/release/v11-changelog', '/welcome/release/v10-changelog', diff --git a/docs/en-US/welcome/getting-started/upgrading/create-nocobase-app.md b/docs/en-US/welcome/getting-started/upgrading/create-nocobase-app.md index ad4facaa3..bbb9d64de 100644 --- a/docs/en-US/welcome/getting-started/upgrading/create-nocobase-app.md +++ b/docs/en-US/welcome/getting-started/upgrading/create-nocobase-app.md @@ -1,8 +1,14 @@ # Upgrading for `create-nocobase-app` -## Minor version upgrade + -Just execute the `nocobase upgrade` upgrade command +After v0.12, apps installed via `create-nocobase-app` no longer have a `packages/app` directory, and code customized in `packages/app` needs to be moved to the custom plugin. + + + +## Upgrading + +After v0.12, upgrading the application can be done by running the `yarn nocobase upgrade` command. ```bash # Switch to the corresponding directory @@ -13,63 +19,4 @@ yarn nocobase upgrade yarn dev ``` -## Major upgrade - -You can also use this upgrade method if a minor upgrade fails. - -### 1. Create a new NocoBase project - -```bash -## SQLite -yarn create nocobase-app my-nocobase-app -d sqlite -# MySQL -yarn create nocobase-app my-nocobase-app -d mysql -# PostgreSQL -yarn create nocobase-app my-nocobase-app -d postgres -``` - -### 2. Switching directories - -```bash -cd my-nocobase-app -``` - -### 3. Install dependencies - -📢 This next step may take more than ten minutes due to network environment, system configuration and other factors. - -```bash -yarn install -``` - -### 4. Modify the .env configuration - -Refer to the old version of .env to modify, the database information needs to be configured correctly. The SQLite database also needs to be copied to the `. /storage/db/` directory. - -### 5. Old code migration (not required) - -Business code refer to the new version of plug-in development tutorial and API reference for modification. - -### 6. Execute the upgrade command - -The code is already the latest version, so you need to skip the code update `--skip-code-update` when upgrading. - -```bash -yarn nocobase upgrade --skip-code-update -``` - -### 7. Start NocoBase - -development environment - -```bash -yarn dev -``` - -Production environment - -```bash -yarn start # Not supported on win platforms yet -``` - -Note: For production environment, if the code has been modified, you need to execute ``yarn build`` and restart NocoBase. +If there are problems with upgrading, you can also [recreate new app](/welcome/getting-started/installation/create-nocobase-app) and refer to the old version of .env to change the environment variables. The database information needs to be configured correctly. When using a SQLite database, you need to copy the database files to the `./storage/db/` directory. Finally, run `yarn nocobase upgrade` to upgrade. diff --git a/docs/en-US/welcome/getting-started/upgrading/git-clone.md b/docs/en-US/welcome/getting-started/upgrading/git-clone.md index 732f8a111..27a6e27f9 100644 --- a/docs/en-US/welcome/getting-started/upgrading/git-clone.md +++ b/docs/en-US/welcome/getting-started/upgrading/git-clone.md @@ -12,16 +12,12 @@ cd my-nocobase-app git pull ``` -## 3. 删除旧依赖文件(非必须) - -v0.10 进行了依赖的重大升级,如果 v0.9 升级 v0.10,需要删掉以下目录之后再升级 +## 3. Delete cache and dependencies (optional) ```bash -# Remove .umi cache -yarn rimraf -rf "./**/{.umi,.umi-production}" -# Delete compiled files -yarn rimraf -rf "./packages/*/*/{lib,esm,es,dist,node_modules}" -# Remove dependencies +# delete nocobase cache +yarn nocobase clean +# delete dependencies yarn rimraf -rf node_modules ``` diff --git a/docs/en-US/welcome/release/v13-changelog.md b/docs/en-US/welcome/release/v13-changelog.md new file mode 100644 index 000000000..166f7e5e4 --- /dev/null +++ b/docs/en-US/welcome/release/v13-changelog.md @@ -0,0 +1,19 @@ +# v0.13: New application status flow + +## New Features + +### Application status flow + + + +### Demo video + + + +## Upgrading + +- [Upgrading for Docker compose](/welcome/getting-started/upgrading/docker-compose) +- [Upgrading for create-nocobase-app](/welcome/getting-started/upgrading/create-nocobase-app) +- [Upgrading for Git source code](/welcome/getting-started/upgrading/git-clone) diff --git a/docs/zh-CN/welcome/getting-started/upgrading/create-nocobase-app.md b/docs/zh-CN/welcome/getting-started/upgrading/create-nocobase-app.md index ee443b043..a0ae06137 100644 --- a/docs/zh-CN/welcome/getting-started/upgrading/create-nocobase-app.md +++ b/docs/zh-CN/welcome/getting-started/upgrading/create-nocobase-app.md @@ -1,12 +1,12 @@ # `create-nocobase-app` 安装的升级 -通过 create-nocobase-app 安装的应用不再有 packages/app 目录了,在 packages/app 里自定义的代码,需要移至自定义插件中。 +v0.12 之后的版本,通过 create-nocobase-app 安装的应用不再有 packages/app 目录了,在 packages/app 里自定义的代码,需要移至自定义插件中。 -## 小版本升级 +## 版本升级 -执行 `nocobase upgrade` 升级命令即可 +v0.12 之后的版本,应用的升级直接执行 `yarn nocobase upgrade` 升级命令即可 ```bash # 切换到对应的目录 @@ -17,63 +17,4 @@ yarn nocobase upgrade yarn dev ``` -## 大版本升级 - -如果小版本升级失效,也可以采用此升级办法。 - -### 1. 创建新的 NocoBase 项目 - -```bash -# SQLite -yarn create nocobase-app my-nocobase-app -d sqlite -# MySQL -yarn create nocobase-app my-nocobase-app -d mysql -# PostgreSQL -yarn create nocobase-app my-nocobase-app -d postgres -``` - -### 2. 切换目录 - -```bash -cd my-nocobase-app -``` - -### 3. 安装依赖 - -📢 由于网络环境、系统配置等因素影响,接下来这一步骤可能需要十几分钟时间。 - -```bash -yarn install -``` - -### 4. 修改 .env 配置 - -参考旧版本的 .env 修改,数据库信息需要配置正确。SQLite 数据库也需要将数据库文件复制到 `./storage/db/` 目录。 - -### 5. 旧代码迁移(非必须) - -业务代码参考新版插件开发教程和 API 参考进行修改。 - -### 6. 执行升级命令 - -代码已经是最新版了,所以 upgrade 时需要跳过代码更新 `--skip-code-update`。 - -```bash -yarn nocobase upgrade --skip-code-update -``` - -### 7. 启动 NocoBase - -开发环境 - -```bash -yarn dev -``` - -生产环境 - -```bash -yarn start -``` - -注:生产环境,如果代码有修改,需要执行 `yarn build`,再重新启动 NocoBase。 +如果升级存在问题,也可以[重新创建新应用](/welcome/getting-started/installation/create-nocobase-app),并参考旧版本的 .env 修改环境变量。数据库信息需要配置正确,使用 SQLite 数据库时,需要将数据库文件复制到 `./storage/db/` 目录。最后再执行 `yarn nocobase upgrade` 进行升级。 diff --git a/docs/zh-CN/welcome/getting-started/upgrading/git-clone.md b/docs/zh-CN/welcome/getting-started/upgrading/git-clone.md index e1dd0258c..cdd72832b 100644 --- a/docs/zh-CN/welcome/getting-started/upgrading/git-clone.md +++ b/docs/zh-CN/welcome/getting-started/upgrading/git-clone.md @@ -12,16 +12,14 @@ cd my-nocobase-app git pull ``` -## 3. 删除旧依赖文件(非必须) +## 3. 删除缓存和旧依赖(非必须) -v0.10 进行了依赖的重大升级,如果 v0.9 升级 v0.10,需要删掉以下目录之后再升级 +如果正常的升级流程失败,可以尝试清空缓存和依赖之后重新下载 ```bash -# 删除 .umi 相关缓存 -yarn rimraf -rf "./**/{.umi,.umi-production}" -# 删除编译文件 -yarn rimraf -rf "./packages/*/*/{lib,esm,es,dist,node_modules}" -# 删除全部依赖 +# 删除 nocobase 缓存 +yarn nocobase clean +# 删除依赖 yarn rimraf -rf node_modules ``` diff --git a/docs/zh-CN/welcome/release/v13-changelog.md b/docs/zh-CN/welcome/release/v13-changelog.md new file mode 100644 index 000000000..ae2d7323c --- /dev/null +++ b/docs/zh-CN/welcome/release/v13-changelog.md @@ -0,0 +1,19 @@ +# v0.13:全新的应用状态流转 + +## 新特性 + +### 应用状态流转 + + + +### 演示视频 + + + +## 应用升级 + +- [Docker 安装的升级](/welcome/getting-started/upgrading/docker-compose) +- [create-nocobase-app 安装的升级](/welcome/getting-started/upgrading/create-nocobase-app) +- [Git 源码安装的升级](/welcome/getting-started/upgrading/git-clone) diff --git a/examples/app/__tests__/app.test.ts b/examples/app/__tests__/app.test.ts index 4f6abefbc..7b3309764 100644 --- a/examples/app/__tests__/app.test.ts +++ b/examples/app/__tests__/app.test.ts @@ -9,8 +9,9 @@ import { MockServer, mockServer } from '@nocobase/test'; describe('app test', () => { let app: MockServer; - beforeEach(() => { + beforeEach(async () => { app = mockServer(); + await app.start(); }); test('test1', async () => { diff --git a/package.json b/package.json index 1a1c1c82a..484c21cea 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "nocobase": "nocobase", "pm": "nocobase pm", "dev": "nocobase dev", + "dev-server": "nocobase dev --server", "start": "nocobase start", "build": "nocobase build", "test": "nocobase test", diff --git a/packages/core/app/client/src/pages/index.tsx b/packages/core/app/client/src/pages/index.tsx index d098cfbec..4381b7fe8 100644 --- a/packages/core/app/client/src/pages/index.tsx +++ b/packages/core/app/client/src/pages/index.tsx @@ -7,6 +7,7 @@ export const app = new Application({ baseURL: process.env.API_BASE_URL, }, plugins: [NocoBaseClientPresetPlugin], + ws: true, devDynamicImport, }); diff --git a/packages/core/app/src/index.ts b/packages/core/app/src/index.ts index 2c9561c38..edf45014b 100644 --- a/packages/core/app/src/index.ts +++ b/packages/core/app/src/index.ts @@ -1,10 +1,6 @@ -import { Application } from '@nocobase/server'; +import { Gateway } from '@nocobase/server'; import config from './config'; -const app = new Application(config); - -if (require.main === module) { - app.runAsCLI(); -} - -export default app; +Gateway.getInstance().run({ + mainAppOptions: config, +}); diff --git a/packages/core/auth/src/__tests__/auth-manager.test.ts b/packages/core/auth/src/__tests__/auth-manager.test.ts index 6640904ac..9deb97e47 100644 --- a/packages/core/auth/src/__tests__/auth-manager.test.ts +++ b/packages/core/auth/src/__tests__/auth-manager.test.ts @@ -68,7 +68,7 @@ describe('auth-manager', () => { }); afterEach(async () => { - await db.close(); + await app.destroy(); }); describe('blacklist', () => { diff --git a/packages/core/cli/package.json b/packages/core/cli/package.json index e5628e324..022f0ea1e 100644 --- a/packages/core/cli/package.json +++ b/packages/core/cli/package.json @@ -17,6 +17,7 @@ "execa": "^5.1.1", "fast-glob": "^3.2.12", "fs-extra": "^11.1.1", + "newrelic": "^10.6.2", "pm2": "^5.2.0", "portfinder": "^1.0.28", "serve": "^13.0.2", diff --git a/packages/core/cli/src/commands/dev.js b/packages/core/cli/src/commands/dev.js index b21464b71..43909c0d2 100644 --- a/packages/core/cli/src/commands/dev.js +++ b/packages/core/cli/src/commands/dev.js @@ -54,8 +54,6 @@ module.exports = (cli) => { }); } - await runAppCommand('install', ['--silent']); - if (server || !client) { console.log('starting server', serverPort); @@ -99,6 +97,7 @@ module.exports = (cli) => { env: { PORT: clientPort, APP_ROOT: `${APP_PACKAGE_ROOT}/client`, + WEBSOCKET_URL: process.env.WEBSOCKET_URL || `ws://localhost:${serverPort}/ws`, PROXY_TARGET_URL: process.env.PROXY_TARGET_URL || (serverPort ? `http://127.0.0.1:${serverPort}` : undefined), }, diff --git a/packages/core/cli/src/commands/global.js b/packages/core/cli/src/commands/global.js index 517c40b4c..bf6e93dc2 100644 --- a/packages/core/cli/src/commands/global.js +++ b/packages/core/cli/src/commands/global.js @@ -12,11 +12,10 @@ module.exports = (cli) => { .option('-h, --help') .option('--ts-node-dev') .action((options) => { - const { tsNodeDev } = options; if (isDev()) { promptForTs(); - run(tsNodeDev ? 'ts-node-dev' : 'ts-node', [ - '-P', + run('tsx', [ + '--tsconfig', './tsconfig.server.json', '-r', 'tsconfig-paths/register', diff --git a/packages/core/cli/src/commands/start.js b/packages/core/cli/src/commands/start.js index f7b003284..a22111a03 100644 --- a/packages/core/cli/src/commands/start.js +++ b/packages/core/cli/src/commands/start.js @@ -41,16 +41,6 @@ module.exports = (cli) => { return; } await postCheck(opts); - const restartMark = resolve(process.cwd(), 'storage', 'restart'); - if (!existsSync(restartMark)) { - if (opts.quickstart) { - await run('node', [`${APP_PACKAGE_ROOT}/lib/index.js`, 'install', '--ignore-installed']); - await run('node', [`${APP_PACKAGE_ROOT}/lib/index.js`, 'upgrade']); - } - if (opts.dbSync) { - await run('node', [`${APP_PACKAGE_ROOT}/lib/index.js`, 'db:sync']); - } - } if (opts.daemon) { run('pm2', ['start', `${APP_PACKAGE_ROOT}/lib/index.js`, '--', ...process.argv.slice(2)]); } else { diff --git a/packages/core/client/src/acl/ACLProvider.tsx b/packages/core/client/src/acl/ACLProvider.tsx index ffb378a3d..d505247a2 100644 --- a/packages/core/client/src/acl/ACLProvider.tsx +++ b/packages/core/client/src/acl/ACLProvider.tsx @@ -1,9 +1,9 @@ import { Field } from '@formily/core'; import { Schema, useField, useFieldSchema } from '@formily/react'; -import { Spin } from 'antd'; import React, { createContext, useContext, useEffect } from 'react'; import { Navigate } from 'react-router-dom'; import { useAPIClient, useRequest } from '../api-client'; +import { useAppSpin } from '../application/hooks/useAppSpin'; import { useBlockRequestContext } from '../block-provider/BlockProvider'; import { useCollection } from '../collection-manager'; import { useResourceActionContext } from '../collection-manager/ResourceActionProvider'; @@ -33,6 +33,7 @@ const getRouteUrl = (props) => { export const ACLRolesCheckProvider = (props) => { const route = getRouteUrl(props.children.props); const { setDesignable } = useDesignable(); + const { render } = useAppSpin(); const api = useAPIClient(); const result = useRequest<{ data: { @@ -60,7 +61,7 @@ export const ACLRolesCheckProvider = (props) => { }, ); if (result.loading) { - return ; + return render(); } if (result.error) { return ; diff --git a/packages/core/client/src/api-client/APIClient.ts b/packages/core/client/src/api-client/APIClient.ts index 94f2f72a1..aa2c019c0 100644 --- a/packages/core/client/src/api-client/APIClient.ts +++ b/packages/core/client/src/api-client/APIClient.ts @@ -2,6 +2,7 @@ import { APIClient as APIClientSDK } from '@nocobase/sdk'; import { Result } from 'ahooks/es/useRequest/src/types'; import { notification } from 'antd'; import React from 'react'; +import { Application } from '../application'; const handleErrorMessage = (error, notification) => { const reader = new FileReader(); @@ -9,7 +10,7 @@ const handleErrorMessage = (error, notification) => { reader.onload = function () { notification.error({ message: JSON.parse(reader.result as string).errors?.map?.((error: any) => { - return React.createElement('div', { children: error.message }); + return React.createElement('div', {}, error.message); }), }); }; @@ -19,6 +20,7 @@ const errorCache = new Map(); export class APIClient extends APIClientSDK { services: Record> = {}; silence = false; + app: Application; /** 该值会在 AntdAppProvider 中被重新赋值 */ notification: any = notification; @@ -56,6 +58,16 @@ export class APIClient extends APIClientSDK { if (errorCache.size > 10) { errorCache.clear(); } + const maintaining = !!error?.response?.data?.error?.maintaining; + if (this.app.maintaining !== maintaining) { + this.app.maintaining = maintaining; + } + if (this.app.maintaining) { + this.app.error = error?.response?.data?.error; + return; + } else if (this.app.error) { + this.app.error = null; + } let errs = error?.response?.data?.errors || [{ message: 'Server error' }]; errs = errs.filter((error) => { const lastTime = errorCache.get(error.message); diff --git a/packages/core/client/src/appInfo/CurrentAppInfoProvider.tsx b/packages/core/client/src/appInfo/CurrentAppInfoProvider.tsx index cb3924ec3..b9391e916 100644 --- a/packages/core/client/src/appInfo/CurrentAppInfoProvider.tsx +++ b/packages/core/client/src/appInfo/CurrentAppInfoProvider.tsx @@ -1,6 +1,6 @@ -import { Spin } from 'antd'; import React, { createContext, useContext } from 'react'; import { useRequest } from '../api-client'; +import { useAppSpin } from '../application/hooks/useAppSpin'; export const CurrentAppInfoContext = createContext(null); @@ -16,11 +16,12 @@ export const useCurrentAppInfo = () => { }>(CurrentAppInfoContext); }; export const CurrentAppInfoProvider = (props) => { + const { render } = useAppSpin(); const result = useRequest({ url: 'app:getInfo', }); if (result.loading) { - return ; + return render(); } return {props.children}; }; diff --git a/packages/core/client/src/application/Application.tsx b/packages/core/client/src/application/Application.tsx index 3ce0ac745..5aaac8993 100644 --- a/packages/core/client/src/application/Application.tsx +++ b/packages/core/client/src/application/Application.tsx @@ -1,3 +1,4 @@ +import { define, observable } from '@formily/reactive'; import { APIClientOptions } from '@nocobase/sdk'; import { i18n as i18next } from 'i18next'; import get from 'lodash/get'; @@ -12,11 +13,12 @@ import { i18n } from '../i18n'; import type { Plugin } from './Plugin'; import { PluginManager, PluginType } from './PluginManager'; import { ComponentTypeAndString, RouterManager, RouterOptions } from './RouterManager'; +import { WebSocketClient, WebSocketClientOptions } from './WebSocketClient'; import { AppComponent, BlankComponent, defaultAppComponents } from './components'; import { compose, normalizeContainer } from './utils'; import { defineGlobalDeps } from './utils/globalDeps'; -import { getRequireJs } from './utils/requirejs'; import type { RequireJS } from './utils/requirejs'; +import { getRequireJs } from './utils/requirejs'; declare global { interface Window { @@ -28,6 +30,7 @@ export type DevDynamicImport = (packageName: string) => Promise<{ default: typeo export type ComponentAndProps = [ComponentType, T]; export interface ApplicationOptions { apiClient?: APIClientOptions; + ws?: WebSocketClientOptions | boolean; i18n?: i18next; providers?: (ComponentType | ComponentAndProps)[]; plugins?: PluginType[]; @@ -42,18 +45,30 @@ export class Application { public router: RouterManager; public scopes: Record = {}; public i18n: i18next; + public ws: WebSocketClient; public apiClient: APIClient; public components: Record = { ...defaultAppComponents }; public pm: PluginManager; public devDynamicImport: DevDynamicImport; public requirejs: RequireJS; + loading = true; + maintained = false; + maintaining = false; + error = null; constructor(protected options: ApplicationOptions = {}) { this.initRequireJs(); + define(this, { + maintained: observable.ref, + loading: observable.ref, + maintaining: observable.ref, + error: observable.ref, + }); this.devDynamicImport = options.devDynamicImport; this.scopes = merge(this.scopes, options.scopes); this.components = merge(this.components, options.components); this.apiClient = new APIClient(options.apiClient); + this.apiClient.app = this; this.i18n = options.i18n || i18n; this.router = new RouterManager({ ...options.router, @@ -63,6 +78,7 @@ export class Application { this.addDefaultProviders(); this.addReactRouterComponents(); this.addProviders(options.providers || []); + this.ws = new WebSocketClient(options.ws); } private initRequireJs() { @@ -109,7 +125,38 @@ export class Application { } async load() { - return this.pm.load(); + this.ws.on('message', (event) => { + const data = JSON.parse(event.data); + console.log(data.payload); + const maintaining = data.type === 'maintaining' && data.payload.code !== 'APP_RUNNING'; + if (maintaining) { + this.maintaining = true; + this.error = data.payload; + } else if (this.maintaining) { + // && !this.maintained + window.location.reload(); + return; + } else { + this.maintaining = false; + this.maintained = true; + this.error = null; + } + }); + this.ws.on('serverDown', () => { + this.maintaining = true; + }); + this.ws.connect(); + try { + this.loading = true; + await this.pm.load(); + } catch (error) { + this.error = { + code: 'LOAD_ERROR', + message: error.message, + }; + console.error(error); + } + this.loading = false; } getComponent(Component: ComponentTypeAndString, isShowError = true): ComponentType | undefined { diff --git a/packages/core/client/src/application/PluginManager.ts b/packages/core/client/src/application/PluginManager.ts index 6285e29ea..8bf052e18 100644 --- a/packages/core/client/src/application/PluginManager.ts +++ b/packages/core/client/src/application/PluginManager.ts @@ -41,7 +41,7 @@ export class PluginManager { private async initRemotePlugins() { try { const res = await this.app.apiClient.request({ url: 'app:getPlugins' }); - const pluginList: PluginData[] = res.data?.data || []; + const pluginList: PluginData[] = res?.data?.data || []; const plugins = await getPlugins({ requirejs: this.app.requirejs, pluginData: pluginList, diff --git a/packages/core/client/src/application/WebSocketClient.ts b/packages/core/client/src/application/WebSocketClient.ts new file mode 100644 index 000000000..9c8300582 --- /dev/null +++ b/packages/core/client/src/application/WebSocketClient.ts @@ -0,0 +1,179 @@ +import { define, observable } from '@formily/reactive'; +import { getSubAppName } from '@nocobase/sdk'; + +export const getWebSocketURL = () => { + if (!process.env.API_BASE_URL) { + return; + } + const subApp = getSubAppName(); + const queryString = subApp ? `?__appName=${subApp}` : ''; + console.log(queryString); + if (process.env.WEBSOCKET_URL) { + return `${process.env.WEBSOCKET_URL}${queryString}`; + } + try { + const url = new URL(process.env.API_BASE_URL); + return `${url.protocol === 'https:' ? 'wss' : 'ws'}://${url.host}/ws${queryString}`; + } catch (error) { + return `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws${queryString}`; + } +}; + +export type WebSocketClientOptions = { + reconnectInterval?: number; + reconnectAttempts?: number; + pingInterval?: number; + url?: string; + protocols?: string | string[]; + onServerDown?: any; +}; + +export class WebSocketClient { + protected _ws: WebSocket; + protected _reconnectTimes = 0; + protected events = []; + protected options: WebSocketClientOptions; + protected enabled: boolean; + connected = false; + serverDown = false; + lastMessage = {}; + + constructor(options: WebSocketClientOptions | boolean) { + if (!options) { + this.enabled = false; + return; + } + this.options = options === true ? {} : options; + this.enabled = true; + define(this, { + serverDown: observable.ref, + connected: observable.ref, + lastMessage: observable.ref, + }); + } + + get reconnectAttempts() { + return this.options?.reconnectAttempts || 30; + } + + get reconnectInterval() { + return this.options?.reconnectInterval || 1000; + } + + get pingInterval() { + return this.options?.pingInterval || 30000; + } + + get readyState() { + if (!this._ws) { + return -1; + } + return this._ws.readyState; + } + + connect() { + if (!this.enabled) { + return; + } + if (this._reconnectTimes === 0) { + console.log('[nocobase-ws]: connecting...'); + } + if (this._reconnectTimes >= this.reconnectAttempts) { + return; + } + if (this.readyState === WebSocket.OPEN) { + return; + } + this._reconnectTimes++; + const ws = new WebSocket(this.options.url || getWebSocketURL(), this.options.protocols); + let pingIntervalTimer: any; + ws.onopen = () => { + console.log('[nocobase-ws]: connected.'); + this.serverDown = false; + if (this._ws) { + this.removeAllListeners(); + } + this._reconnectTimes = 0; + this._ws = ws; + for (const { type, listener, options } of this.events) { + this._ws.addEventListener(type, listener, options); + } + pingIntervalTimer = setInterval(() => this.send('ping'), this.pingInterval); + this.connected = true; + }; + ws.onerror = async () => { + // setTimeout(() => this.connect(), this.reconnectInterval); + console.log('onerror', this.readyState, this._reconnectTimes); + }; + ws.onclose = async (event) => { + setTimeout(() => this.connect(), this.reconnectInterval); + console.log('onclose', this.readyState, this._reconnectTimes, this.serverDown); + this.connected = false; + clearInterval(pingIntervalTimer); + if (this._reconnectTimes >= Math.min(this.reconnectAttempts, 5)) { + this.serverDown = true; + this.emit('serverDown', event); + } + }; + } + + reconnect() { + this._reconnectTimes = 0; + this.connect(); + } + + close() { + if (!this._ws) { + return; + } + this._reconnectTimes = this.reconnectAttempts; + return this._ws.close(); + } + + send(data: string | ArrayBufferLike | Blob | ArrayBufferView) { + if (!this._ws) { + return; + } + return this._ws.send(data); + } + + on(type: string, listener: any, options?: boolean | AddEventListenerOptions) { + this.events.push({ type, listener, options }); + if (!this._ws) { + return; + } + this._ws.addEventListener(type, listener, options); + } + + emit(type: string, args: any) { + for (const event of this.events) { + if (event.type === type) { + event.listener(args); + } + } + } + + off(type: string, listener: any, options?: boolean | EventListenerOptions) { + let index = 0; + for (const event of this.events) { + if (event.type === type && event.listener === listener) { + this.events.splice(index, 1); + break; + } + index++; + } + if (!this._ws) { + return; + } + this._ws.removeEventListener(type, listener, options); + } + + removeAllListeners() { + if (!this._ws) { + return; + } + for (const { type, listener, options } of this.events) { + this._ws.removeEventListener(type, listener, options); + } + } +} diff --git a/packages/core/client/src/application/components/AppComponent.tsx b/packages/core/client/src/application/components/AppComponent.tsx index b00fc6e3c..9b596038c 100644 --- a/packages/core/client/src/application/components/AppComponent.tsx +++ b/packages/core/client/src/application/components/AppComponent.tsx @@ -1,25 +1,33 @@ -import React, { FC, useCallback } from 'react'; +import { observer } from '@formily/reactive-react'; +import React, { FC, useCallback, useEffect } from 'react'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; import type { Application } from '../Application'; import { ApplicationContext } from '../context'; -import { useAppPluginLoad } from '../hooks'; export interface AppComponentProps { app: Application; } -export const AppComponent: FC = (props) => { +export const AppComponent: FC = observer((props) => { const { app } = props; - const { loading, error } = useAppPluginLoad(app); const handleErrors = useCallback((error: Error, info: { componentStack: string }) => { console.error(error, info); }, []); - if (loading) return app.renderComponent('AppSpin', { app }); - if (error) return app.renderComponent('AppError', { app, error }); + + useEffect(() => { + app.load(); + }, [app]); + + if (app.loading) return app.renderComponent('AppSpin', { app }); + if (!app.maintained && app.maintaining) return app.renderComponent('AppMaintaining', { app }); + if (app.error?.code === 'LOAD_ERROR') return app.renderComponent('AppError', { app }); return ( ('ErrorFallback')} onError={handleErrors}> - {app.renderComponent('AppMain')} + + {app.maintained && app.maintaining && app.renderComponent('AppMaintainingDialog', { app })} + {app.renderComponent('AppMain')} + ); -}; +}); diff --git a/packages/core/client/src/application/context.ts b/packages/core/client/src/application/context.ts index cb7e59d69..34d49a1a4 100644 --- a/packages/core/client/src/application/context.ts +++ b/packages/core/client/src/application/context.ts @@ -1,4 +1,4 @@ import { createContext } from 'react'; -import type { Application } from './Application'; +import { Application } from './Application'; export const ApplicationContext = createContext(null); diff --git a/packages/core/client/src/application/hooks/useAppPluginLoad.ts b/packages/core/client/src/application/hooks/useAppPluginLoad.ts index 09cb971c6..6302a0c11 100644 --- a/packages/core/client/src/application/hooks/useAppPluginLoad.ts +++ b/packages/core/client/src/application/hooks/useAppPluginLoad.ts @@ -5,6 +5,9 @@ export function useAppPluginLoad(app: Application) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { + app.ws.on('message', (event) => { + console.log(event.data); + }); async function run() { try { await app.load(); diff --git a/packages/core/client/src/application/hooks/useAppSpin.ts b/packages/core/client/src/application/hooks/useAppSpin.ts new file mode 100644 index 000000000..28469aa10 --- /dev/null +++ b/packages/core/client/src/application/hooks/useAppSpin.ts @@ -0,0 +1,10 @@ +import { Spin } from 'antd'; +import React from 'react'; +import { useApp } from './useApp'; + +export const useAppSpin = () => { + const app = useApp(); + return { + render: () => (app ? app?.renderComponent?.('AppSpin') : React.createElement(Spin)), + }; +}; diff --git a/packages/core/client/src/collection-manager/CollectionHistoryProvider.tsx b/packages/core/client/src/collection-manager/CollectionHistoryProvider.tsx index 56a861ca7..3773fed52 100644 --- a/packages/core/client/src/collection-manager/CollectionHistoryProvider.tsx +++ b/packages/core/client/src/collection-manager/CollectionHistoryProvider.tsx @@ -1,7 +1,7 @@ -import { Spin } from 'antd'; import React, { createContext, useContext, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { useAPIClient, useRequest } from '../api-client'; +import { useAppSpin } from '../application/hooks/useAppSpin'; export interface CollectionHistoryContextValue { historyCollections: any[]; @@ -41,6 +41,7 @@ export const CollectionHistoryProvider: React.FC = (props) => { const isAdminPage = location.pathname.startsWith('/admin'); const token = api.auth.getToken() || ''; + const { render } = useAppSpin(); useEffect(() => { if (isAdminPage && token) { @@ -56,7 +57,7 @@ export const CollectionHistoryProvider: React.FC = (props) => { }; if (service.loading) { - return ; + return render(); } return ( diff --git a/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx b/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx index 58ac3ea42..4d2cb2318 100644 --- a/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx +++ b/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx @@ -1,8 +1,8 @@ -import { Spin } from 'antd'; import { keyBy } from 'lodash'; import React, { useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAPIClient, useRequest } from '../api-client'; +import { useAppSpin } from '../application/hooks/useAppSpin'; import { templateOptions } from '../collection-manager/Configuration/templates'; import { useCollectionHistory } from './CollectionHistoryProvider'; import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider'; @@ -36,6 +36,7 @@ export const RemoteCollectionManagerProvider = (props: any) => { const api = useAPIClient(); const [contentLoading, setContentLoading] = useState(false); const { refreshCH } = useCollectionHistory(); + const { render } = useAppSpin(); const options = { resource: 'collections', action: 'list', @@ -63,7 +64,7 @@ export const RemoteCollectionManagerProvider = (props: any) => { }>(coptions); if (service.loading) { - return ; + return render(); } const refreshCM = async (opts) => { if (opts?.reload) { diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 7a5c7bd47..4ddaf9cae 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -769,7 +769,7 @@ export default { 'Reload application': '重载应用', 'The application is reloading, please do not close the page.': '应用正在重新加载,请勿关闭页面。', 'Application reloading': '应用重新加载中', - 'Reboot application': '重启应用', + 'Restart application': '重启应用', "Allows to clear cache, reboot application": "允许清除缓存,重启应用", 'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?': '重启将会中断当前服务,这个过程可能需要一点时间,确定要继续吗?', 'Reboot': '重启', diff --git a/packages/core/client/src/nocobase-buildin-plugin/index.tsx b/packages/core/client/src/nocobase-buildin-plugin/index.tsx index d896b3d5e..78ea9904f 100644 --- a/packages/core/client/src/nocobase-buildin-plugin/index.tsx +++ b/packages/core/client/src/nocobase-buildin-plugin/index.tsx @@ -1,5 +1,7 @@ +import { LoadingOutlined } from '@ant-design/icons'; import { css } from '@emotion/css'; -import { Button, Result, Spin } from 'antd'; +import { observer } from '@formily/reactive-react'; +import { Button, Modal, Result, Spin } from 'antd'; import React, { FC } from 'react'; import { Navigate } from 'react-router-dom'; import { ACLPlugin } from '../acl'; @@ -21,9 +23,13 @@ import { SystemSettingsPlugin } from '../system-settings'; import { CurrentUserProvider, CurrentUserSettingsMenuProvider } from '../user'; import { LocalePlugin } from './plugins/LocalePlugin'; -const AppSpin = Spin; +const AppSpin = () => { + return ( + + ); +}; -const AppError: FC<{ app: Application; error: Error }> = ({ app, error }) => ( +const AppError: FC<{ app: Application }> = observer(({ app }) => (
= ({ app, error }) => ( `} status="error" title={app.i18n.t('Failed to load plugin')} - subTitle={app.i18n.t(error?.message)} + subTitle={app.i18n.t(app.error?.message)} extra={[
-); +)); + +const getProps = (app: Application) => { + if (app.ws.serverDown) { + return { + status: 'error', + title: 'App error', + subTitle: 'The server is down', + }; + } + + if (!app.error) { + return {}; + } + + if (app.error.code === 'APP_NOT_FOUND') { + return { + status: 'warning', + title: 'App not found', + subTitle: app.error?.message, + }; + } + + if (app.error.code === 'APP_INITIALIZING') { + return { + status: 'info', + icon: , + title: 'App initializing', + subTitle: app.error?.message, + }; + } + + if (app.error.code === 'APP_INITIALIZED') { + return { + status: 'warning', + title: 'App initialized', + subTitle: app.error?.message, + }; + } + + if (app.error.code === 'APP_ERROR') { + return { + status: 'error', + title: 'App error', + subTitle: app.error?.message, + }; + } + + if (app.error.code === 'APP_NOT_INSTALLED_ERROR') { + return { + status: 'warning', + title: 'App not installed', + subTitle: app.error?.message, + }; + } + + if (app.error.code === 'APP_STOPPED') { + return { + status: 'warning', + title: 'App stopped', + subTitle: app.error?.message, + }; + } + + if (app.error.code === 'APP_COMMANDING') { + const props = { + status: 'info', + icon: , + title: app.error?.command?.name, + subTitle: app.error?.message, + }; + const commands = { + start: { + title: 'App starting', + }, + restart: { + title: 'App restarting', + }, + install: { + title: 'App installing', + }, + upgrade: { + title: 'App upgrading', + }, + 'pm.enable': { + title: 'Enabling plugin', + }, + 'pm.disable': { + title: 'Disabling plugin', + }, + }; + return { ...props, ...commands[app.error?.command?.name] }; + } +}; + +const AppMaintaining: FC<{ app: Application; error: Error }> = observer(({ app }) => { + const { icon, status, title, subTitle } = getProps(app); + return ( +
+ window.location.reload()}> + // {app.i18n.t('Try again')} + // , + // ]} + /> +
+ ); +}); + +const AppMaintainingDialog: FC<{ app: Application; error: Error }> = observer(({ app }) => { + const { icon, status, title, subTitle } = getProps(app); + return ( + + + + ); +}); export class NocoBaseBuildInPlugin extends Plugin { async afterAdd() { this.app.addComponents({ AppSpin, AppError, + AppMaintaining, + AppMaintainingDialog, }); await this.addPlugins(); } diff --git a/packages/core/client/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts b/packages/core/client/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts index 4d1d01a10..7bdbcfa3f 100644 --- a/packages/core/client/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts +++ b/packages/core/client/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts @@ -10,12 +10,13 @@ export class LocalePlugin extends Plugin { const api = this.app.apiClient; const locale = api.auth.locale; try { - const { data } = await api.request({ + const res = await api.request({ url: 'app:getLang', params: { locale, }, }); + const data = res?.data; this.locales = data?.data || {}; this.app.use(ConfigProvider, { locale: this.locales.antd, popupMatchSelectWidth: false }); if (data?.data?.lang && !locale) { diff --git a/packages/core/client/src/pm/Card.tsx b/packages/core/client/src/pm/Card.tsx index edad0a032..c80ef77af 100644 --- a/packages/core/client/src/pm/Card.tsx +++ b/packages/core/client/src/pm/Card.tsx @@ -247,7 +247,7 @@ export const PluginCard = (props: { data: IPluginData }) => { url: `pm:remove/${name}`, }); message.success(t('插件删除成功')); - window.location.reload(); + // window.location.reload(); }} onCancel={(e) => e.stopPropagation()} okText={t('Yes')} @@ -260,19 +260,19 @@ export const PluginCard = (props: { data: IPluginData }) => { size={'small'} onChange={async (checked, e) => { e.stopPropagation(); - modal.warning({ - title: checked ? t('Plugin starting') : t('Plugin stopping'), - content: t('The application is reloading, please do not close the page.'), - okButtonProps: { - style: { - display: 'none', - }, - }, - }); + // modal.warning({ + // title: checked ? t('Plugin starting') : t('Plugin stopping'), + // content: t('The application is reloading, please do not close the page.'), + // okButtonProps: { + // style: { + // display: 'none', + // }, + // }, + // }); await api.request({ url: `pm:${checked ? 'enable' : 'disable'}/${name}`, }); - window.location.reload(); + // window.location.reload(); // message.success(checked ? t('插件激活成功') : t('插件禁用成功')); }} defaultChecked={enabled} diff --git a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx index 3f0965230..3cf548632 100644 --- a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx +++ b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { Layout, Spin } from 'antd'; +import { Layout } from 'antd'; import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { Outlet, useMatch, useNavigate, useParams } from 'react-router-dom'; import { @@ -22,6 +22,7 @@ import { useToken, } from '../../../'; import { Plugin } from '../../../application/Plugin'; +import { useAppSpin } from '../../../application/hooks/useAppSpin'; import { useCollectionManager } from '../../../collection-manager'; const filterByACL = (schema, options) => { @@ -74,7 +75,7 @@ const MenuEditor = (props) => { setCurrent(schema); navigate(`/admin/${schema['x-uid']}`); }; - + const { render } = useAppSpin(); const adminSchemaUid = useAdminSchemaUid(); const { data, loading } = useRequest<{ data: any; @@ -140,7 +141,7 @@ const MenuEditor = (props) => { return s; }, [data?.data]); if (loading) { - return ; + return render(); } return ( @@ -155,6 +156,7 @@ export const InternalAdminLayout = (props: any) => { const { service } = useCollectionManager(); const params = useParams(); const { token } = useToken(); + const { render } = useAppSpin(); return ( @@ -299,7 +301,7 @@ export const InternalAdminLayout = (props: any) => { pointer-events: none; `} > - {service.contentLoading ? : } + {service.contentLoading ? render() : } ); diff --git a/packages/core/client/src/schema-component/antd/page/Page.tsx b/packages/core/client/src/schema-component/antd/page/Page.tsx index b8ad9457c..0fcf3e010 100644 --- a/packages/core/client/src/schema-component/antd/page/Page.tsx +++ b/packages/core/client/src/schema-component/antd/page/Page.tsx @@ -2,13 +2,14 @@ import { PlusOutlined } from '@ant-design/icons'; import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout'; import { FormLayout } from '@formily/antd-v5'; import { Schema, SchemaOptionsContext, useFieldSchema } from '@formily/react'; -import { Button, Spin, Tabs } from 'antd'; +import { Button, Tabs } from 'antd'; import classNames from 'classnames'; import React, { useContext, useEffect, useMemo, useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; import { FormDialog } from '..'; +import { useAppSpin } from '../../../application/hooks/useAppSpin'; import { useDocumentTitle } from '../../../document-title'; import { FilterBlockProvider } from '../../../filter-provider/FilterProvider'; import { useGlobalTheme } from '../../../global-theme'; @@ -194,9 +195,10 @@ function PageContent( props: any, ): React.ReactNode { const { token } = useToken(); + const { render } = useAppSpin(); if (loading) { - return ; + return render(); } return !disablePageHeader && enablePageTabs ? ( diff --git a/packages/core/client/src/schema-templates/SchemaTemplateManagerProvider.tsx b/packages/core/client/src/schema-templates/SchemaTemplateManagerProvider.tsx index cf09dc19c..89a182e20 100644 --- a/packages/core/client/src/schema-templates/SchemaTemplateManagerProvider.tsx +++ b/packages/core/client/src/schema-templates/SchemaTemplateManagerProvider.tsx @@ -1,10 +1,10 @@ import { ISchema, useFieldSchema } from '@formily/react'; import { uid } from '@formily/shared'; -import { Spin } from 'antd'; import { cloneDeep } from 'lodash'; -import React, { createContext, ReactNode, useContext, useMemo } from 'react'; +import React, { ReactNode, createContext, useContext, useMemo } from 'react'; import { useAPIClient, useRequest } from '../api-client'; import { Plugin } from '../application/Plugin'; +import { useAppSpin } from '../application/hooks/useAppSpin'; import { useCollectionManager } from '../collection-manager'; import { BlockTemplate } from './BlockTemplate'; @@ -116,6 +116,7 @@ export const useSchemaTemplateManager = () => { export const RemoteSchemaTemplateManagerProvider: React.FC<{ children?: ReactNode }> = (props) => { const api = useAPIClient(); + const { render } = useAppSpin(); const options = { resource: 'uiSchemaTemplates', action: 'list', @@ -128,7 +129,7 @@ export const RemoteSchemaTemplateManagerProvider: React.FC<{ children?: ReactNod data: any[]; }>(options); if (service.loading) { - return ; + return render(); } return ( >(null); @@ -10,11 +10,12 @@ export const useSystemSettings = () => { }; export const SystemSettingsProvider: React.FC<{ children?: ReactNode }> = (props) => { + const { render } = useAppSpin(); const result = useRequest({ url: 'systemSettings:get/1?appends=logo', }); if (result.loading) { - return ; + return render(); } return {props.children}; }; diff --git a/packages/core/client/src/user/CurrentUser.tsx b/packages/core/client/src/user/CurrentUser.tsx index b3b85c299..145548faa 100644 --- a/packages/core/client/src/user/CurrentUser.tsx +++ b/packages/core/client/src/user/CurrentUser.tsx @@ -78,19 +78,17 @@ export const SettingsMenu: React.FC<{ }, { key: 'reboot', - label: t('Reboot application'), + label: t('Restart application'), onClick: async () => { modal.confirm({ - title: t('Reboot application'), - content: t('The will interrupt service, it may take a few seconds to restart. Are you sure to continue?'), - okText: t('Reboot'), + title: t('Restart application'), + // content: t('The will interrupt service, it may take a few seconds to restart. Are you sure to continue?'), + okText: t('Restart'), okButtonProps: { danger: true, }, onOk: async () => { - await api.resource('app').reboot(); - await check(); - window.location.reload(); + await api.resource('app').restart(); }, }); }, diff --git a/packages/core/client/src/user/CurrentUserProvider.tsx b/packages/core/client/src/user/CurrentUserProvider.tsx index 811b71d76..b0baa6d96 100644 --- a/packages/core/client/src/user/CurrentUserProvider.tsx +++ b/packages/core/client/src/user/CurrentUserProvider.tsx @@ -1,8 +1,8 @@ -import { Spin } from 'antd'; import React, { createContext, useContext, useMemo } from 'react'; import { Navigate, useLocation } from 'react-router-dom'; import { useACLRoleContext } from '../acl'; import { ReturnTypeOfUseRequest, useRequest } from '../api-client'; +import { useAppSpin } from '../application/hooks/useAppSpin'; import { useCompile } from '../schema-component'; export const CurrentUserContext = createContext(null); @@ -29,11 +29,12 @@ export const useCurrentRoles = () => { }; export const CurrentUserProvider = (props) => { + const { render } = useAppSpin(); const result = useRequest({ url: 'auth:check', }); if (result.loading) { - return ; + return render(); } return {props.children}; }; diff --git a/packages/core/devtools/umiConfig.js b/packages/core/devtools/umiConfig.js index 59c5868fc..69949ac7c 100644 --- a/packages/core/devtools/umiConfig.js +++ b/packages/core/devtools/umiConfig.js @@ -35,6 +35,7 @@ function getUmiConfig() { 'process.env.API_BASE_URL': API_BASE_URL || API_BASE_PATH, 'process.env.APP_ENV': process.env.APP_ENV, 'process.env.VERSION': packageJson.version, + 'process.env.WEBSOCKET_URL': process.env.WEBSOCKET_URL, }, // only proxy when using `umi dev` // if the assets are built, will not proxy diff --git a/packages/core/sdk/src/getSubAppName.ts b/packages/core/sdk/src/getSubAppName.ts index f151d85de..ec79fe7b8 100644 --- a/packages/core/sdk/src/getSubAppName.ts +++ b/packages/core/sdk/src/getSubAppName.ts @@ -1,9 +1,9 @@ const getSubAppName = () => { - const match = window.location.pathname.match(/^\/apps\/([^/]*)\//); + const match = window.location.pathname.match(/^\/apps\/([^/]*)\/?/); if (!match) { return ''; } - return match[1].toUpperCase(); + return match[1]; }; export default getSubAppName; diff --git a/packages/core/server/package.json b/packages/core/server/package.json index 515583520..8cdd0ee01 100644 --- a/packages/core/server/package.json +++ b/packages/core/server/package.json @@ -36,10 +36,15 @@ "lodash": "^4.17.21", "multer": "^1.4.2", "semver": "^7.3.7", - "xpipe": "^1.0.5" + "serve-handler": "^6.1.5", + "xpipe": "^1.0.5", + "ws": "^8.13.0", + "nanoid": "3.3.4" }, "devDependencies": { - "@types/semver": "^7.3.9" + "@types/semver": "^7.3.9", + "@types/ws": "^8.5.5", + "@types/serve-handler": "^6.1.1" }, "gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644" } diff --git a/packages/core/server/src/__tests__/app-life-cycle.test.ts b/packages/core/server/src/__tests__/app-life-cycle.test.ts new file mode 100644 index 000000000..d21544d9d --- /dev/null +++ b/packages/core/server/src/__tests__/app-life-cycle.test.ts @@ -0,0 +1,157 @@ +import Application, { ApplicationOptions } from '../application'; +import { createAppProxy } from '../helper'; +import Plugin from '../plugin'; +import { AddPresetError } from '../plugin-manager'; + +const mockServer = (options?: ApplicationOptions) => { + return new Application({ + database: { + dialect: 'sqlite', + storage: ':memory:', + }, + ...options, + }); +}; + +describe('application life cycle', () => { + let app: Application; + + afterEach(async () => { + if (app) { + await app.destroy(); + } + }); + + describe('reInitEvents', () => { + it('should be called', async () => { + app = mockServer(); + const loadFn = jest.fn(); + app.on('event1', () => { + loadFn(); + }); + app.reInitEvents(); + app.emit('event1'); + expect(loadFn).toBeCalled(); + expect(loadFn).toBeCalledTimes(1); + }); + + it('should not be called', async () => { + app = createAppProxy(mockServer()); + const loadFn = jest.fn(); + app.on('event1', () => { + loadFn(); + }); + app.reInitEvents(); + app.emit('event1'); + expect(loadFn).not.toBeCalled(); + }); + + it('should not be called', async () => { + app = createAppProxy(mockServer()); + const loadFn = jest.fn(); + + app.on('event1', () => { + loadFn(); + }); + + app.on('event1', () => { + loadFn(); + }); + + expect(app.listenerCount('event1')).toBe(2); + app.reInitEvents(); + expect(app.listenerCount('event1')).toBe(0); + }); + }); + + describe('load', () => { + it('should be called', async () => { + app = mockServer(); + const loadFn = jest.fn(); + app.on('beforeLoad', () => { + loadFn(); + }); + + await app.load(); + await app.load(); + expect(loadFn).toBeCalled(); + expect(loadFn).toBeCalledTimes(1); + await app.reload(); + await app.reload(); + + expect(app.listenerCount('beforeLoad')).toBe(1); + + expect(loadFn).toBeCalledTimes(3); + }); + it('should be called', async () => { + app = mockServer(); + const loadFn = jest.fn(); + class Plugin1 extends Plugin { + afterAdd() { + this.app.on('beforeLoad', () => { + loadFn(); + }); + } + } + app.pm.addPreset(Plugin1); + await app.load(); + await app.load(); + expect(loadFn).toBeCalled(); + expect(loadFn).toBeCalledTimes(1); + await app.reload(); + await app.reload(); + expect(loadFn).toBeCalledTimes(3); + }); + }); + + describe('addPreset', () => { + it('should remove listener that add by plugin', async () => { + function testFunc() {} + class Plugin1 extends Plugin { + async beforeLoad() { + this.app.on('afterLoad', testFunc); + } + } + + app = mockServer({ + plugins: [Plugin1], + }); + + await app.load(); + expect(app.listeners('afterLoad').filter((fn) => fn.name == testFunc.name)).toHaveLength(1); + await app.reload(); + expect(app.listeners('afterLoad').filter((fn) => fn.name == testFunc.name)).toHaveLength(1); + }); + + it('should init after app.load()', async () => { + class Plugin1 extends Plugin {} + app = mockServer({ + plugins: [Plugin1], + }); + expect(app.pm.has(Plugin1)).toBeFalsy(); + await app.load(); + expect(app.pm.has(Plugin1)).toBeTruthy(); + }); + + it('should init after app.load()', async () => { + class Plugin1 extends Plugin {} + class Plugin2 extends Plugin {} + app = mockServer({ + plugins: [Plugin1], + }); + app.pm.addPreset(Plugin2); + expect(app.pm.has(Plugin1)).toBeFalsy(); + expect(app.pm.has(Plugin2)).toBeFalsy(); + await app.load(); + expect(app.pm.has(Plugin1)).toBeTruthy(); + expect(app.pm.has(Plugin2)).toBeTruthy(); + }); + + it('should throw error', async () => { + class Plugin1 extends Plugin {} + app = mockServer(); + await app.load(); + expect(() => app.pm.addPreset(Plugin1)).toThrow(AddPresetError); + }); + }); +}); diff --git a/packages/core/server/src/__tests__/app-supervisor.test.ts b/packages/core/server/src/__tests__/app-supervisor.test.ts new file mode 100644 index 000000000..be5d93af5 --- /dev/null +++ b/packages/core/server/src/__tests__/app-supervisor.test.ts @@ -0,0 +1,29 @@ +import { AppSupervisor } from '../app-supervisor'; + +describe('App Supervisor', () => { + let appSupervisor: AppSupervisor; + + beforeEach(() => { + appSupervisor = AppSupervisor.getInstance(); + }); + + afterEach(async () => { + await appSupervisor.destroy(); + }); + + it('should get application initializing status', async () => { + expect(appSupervisor.getAppStatus('test')).toBe(undefined); + + appSupervisor.setAppBootstrapper(async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + }); + + appSupervisor.getApp('test'); + + await new Promise((resolve) => setTimeout(resolve, 500)); + expect(appSupervisor.getAppStatus('test')).toBe('initializing'); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + expect(appSupervisor.getAppStatus('test')).toBe('not_found'); + }); +}); diff --git a/packages/core/server/src/__tests__/app.test.ts b/packages/core/server/src/__tests__/app.test.ts new file mode 100644 index 000000000..e63a8c814 --- /dev/null +++ b/packages/core/server/src/__tests__/app.test.ts @@ -0,0 +1,106 @@ +import { DataTypes, mockDatabase } from '@nocobase/database'; +import Application, { ApplicationOptions } from '../application'; +import Plugin from '../plugin'; + +const mockServer = (options?: ApplicationOptions) => { + return new Application({ + database: mockDatabase(), + acl: false, + ...options, + }); +}; + +describe('app destroy', () => { + let app: Application; + + afterEach(async () => { + if (app) { + await app.destroy(); + } + }); + + test('case1', async () => { + app = mockServer(); + await app.load(); + await app.install(); + app.pm.collection.addField('foo', { type: 'string' }); + await app.upgrade(); + const exists = await app.pm.collection.getField('foo').existsInDb(); + expect(exists).toBeTruthy(); + }); + + test('case2', async () => { + app = mockServer(); + await app.load(); + app.db.addMigration({ + name: 'test', + up: () => { + console.log('up...'); + }, + }); + await app.install(); + app.pm.collection.addField('foo', { type: 'string' }); + await app.upgrade(); + const exists = await app.pm.collection.getField('foo').existsInDb(); + expect(exists).toBeTruthy(); + }); + + test('case3', async () => { + app = mockServer(); + await app.load(); + const tableNameWithSchema = app.db.getCollection('applicationPlugins').getTableNameWithSchema(); + app.db.addMigration({ + name: 'test', + up: async () => { + await app.db.sequelize.getQueryInterface().addColumn(tableNameWithSchema, 'foo', { + type: DataTypes.STRING, + }); + await app.db.sequelize.getQueryInterface().addConstraint(tableNameWithSchema, { + type: 'unique', + fields: ['foo'], + }); + }, + }); + await app.install(); + app.pm.collection.addField('foo', { type: 'string', unique: true }); + await app.upgrade(); + const exists = await app.pm.collection.getField('foo').existsInDb(); + expect(exists).toBeTruthy(); + }); + + test('case4', async () => { + class P extends Plugin { + async load() { + this.db.collection({ + name: 'test', + fields: [], + }); + } + } + app = mockServer({ + plugins: [P], + }); + await app.load(); + await app.install(); + await app.db.getRepository('test').create({ values: {} }); + await app.install(); + expect(await app.db.getRepository('test').count()).toBe(1); + await app.install({ clean: true }); + expect(await app.db.getRepository('test').count()).toBe(0); + }); + + test('app main already exists', async () => { + mockServer(); + expect(() => mockServer()).toThrow('app main already exists'); + }); + + test('command', async () => { + const loadFn = jest.fn(); + app = mockServer(); + const command = app.command('foo'); + command.command('bar').action(() => loadFn()); + await app.runCommand('foo', 'bar'); + expect(loadFn).toBeCalled(); + expect(loadFn).toBeCalledTimes(1); + }); +}); diff --git a/packages/core/server/src/__tests__/application.test.ts b/packages/core/server/src/__tests__/application.test.ts index 2cebdccab..31fb19e34 100644 --- a/packages/core/server/src/__tests__/application.test.ts +++ b/packages/core/server/src/__tests__/application.test.ts @@ -47,7 +47,7 @@ describe('application', () => { }); afterEach(async () => { - return app.db.close(); + return app.destroy(); }); it('resourcer.define', async () => { @@ -126,5 +126,26 @@ describe('application', () => { expect(response.body).toEqual([1, 2]); }); - it('should create application with plugins config', async () => {}); + it('should call application with command', async () => { + await app.runCommand('start'); + const jestFn = jest.fn(); + + app.on('beforeInstall', async () => { + jestFn(); + }); + + const runningJest = jest.fn(); + + app.on('maintaining', ({ status }) => { + if (status === 'command_running') { + runningJest(); + } + }); + + await app.runCommand('install'); + + expect(runningJest).toBeCalledTimes(1); + + expect(jestFn).toBeCalledTimes(1); + }); }); diff --git a/packages/core/server/src/__tests__/config.test.ts b/packages/core/server/src/__tests__/config.test.ts index ad5920785..17e9aef9a 100644 --- a/packages/core/server/src/__tests__/config.test.ts +++ b/packages/core/server/src/__tests__/config.test.ts @@ -35,6 +35,8 @@ describe('config', () => { const app = new Application(config); + await app.load(); + const appPluginA = app.getPlugin('test-a'); expect(appPluginA).toBeInstanceOf(TestA); diff --git a/packages/core/server/src/__tests__/dataWrapping.test.ts b/packages/core/server/src/__tests__/dataWrapping.test.ts index a04e65e6f..9e6eb504b 100644 --- a/packages/core/server/src/__tests__/dataWrapping.test.ts +++ b/packages/core/server/src/__tests__/dataWrapping.test.ts @@ -43,7 +43,7 @@ describe('application', () => { }); afterEach(async () => { - return app.db.close(); + return app.destroy(); }); it('resourcer.define', async () => { diff --git a/packages/core/server/src/__tests__/gateway.test.ts b/packages/core/server/src/__tests__/gateway.test.ts new file mode 100644 index 000000000..344401764 --- /dev/null +++ b/packages/core/server/src/__tests__/gateway.test.ts @@ -0,0 +1,314 @@ +import { startServerWithRandomPort, supertest, waitSecond } from '@nocobase/test'; +import { Gateway } from '../gateway'; +import Application from '../application'; +import ws from 'ws'; +import { errors } from '../gateway/errors'; +import { AppSupervisor } from '../app-supervisor'; + +describe('gateway', () => { + let gateway: Gateway; + + beforeEach(() => { + gateway = Gateway.getInstance(); + }); + + afterEach(async () => { + await gateway.destroy(); + await AppSupervisor.getInstance().destroy(); + }); + + describe('http api', () => { + it('should return error when app not found', async () => { + const res = await supertest.agent(gateway.getCallback()).get('/api/app:getInfo'); + expect(res.status).toBe(503); + const data = res.body; + + expect(data).toMatchObject({ + error: { + code: 'APP_INITIALIZING', + message: `application main is initializing`, + status: 503, + maintaining: true, + }, + }); + + const res2 = await supertest.agent(gateway.getCallback()).get('/api/app:getInfo'); + expect(res2.status).toBe(404); + const data2 = res2.body; + + expect(data2).toMatchObject({ + error: { + code: 'APP_NOT_FOUND', + message: `application main not found`, + status: 404, + maintaining: true, + }, + }); + }); + + it('should match error structure', async () => { + const main = new Application({ + database: { + dialect: 'sqlite', + storage: ':memory:', + }, + }); + + const res = await supertest.agent(gateway.getCallback()).get('/api/app:getInfo'); + expect(res.status).toBe(503); + const data = res.body; + + expect(data).toMatchObject({ + error: { + code: 'APP_INITIALIZED', + message: errors.APP_INITIALIZED.message({ app: main }), + status: 503, + maintaining: true, + }, + }); + }); + + it('should return error when app not installed', async () => { + const main = new Application({ + database: { + dialect: 'sqlite', + storage: ':memory:', + }, + }); + + // app should have error when not installed + await main.runAsCLI(['start'], { from: 'user' }); + + const res = await supertest.agent(gateway.getCallback()).get('/api/app:getInfo'); + expect(res.status).toBe(503); + const data = res.body; + + expect(data).toMatchObject({ + error: { + code: 'APP_NOT_INSTALLED_ERROR', + message: errors.APP_ERROR.message({ app: main }), + command: { + name: 'start', + }, + status: 503, + maintaining: true, + }, + }); + }); + + it('should return running message when app is command running status', async () => { + const main = new Application({ + database: { + dialect: 'sqlite', + storage: ':memory:', + }, + }); + + main.on('beforeInstall', async () => { + await new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + }); + + const installPromise = main.runAsCLI(['install'], { from: 'user' }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const res = await supertest.agent(gateway.getCallback()).get('/api/app:getInfo'); + const data = res.body; + + expect(data).toMatchObject({ + error: { + status: 503, + maintaining: true, + message: 'call beforeInstall hook...', + code: 'APP_COMMANDING', + command: { + name: 'install', + }, + }, + }); + + await installPromise; + }); + }); + + describe('websocket api', () => { + let wsClient; + let port; + + let messages: Array; + + const connectClient = (port) => { + wsClient = new ws(`ws://localhost:${port}/ws`); + wsClient.on('message', (data) => { + const message = data.toString(); + messages.push(message); + }); + + // await connection established + return new Promise((resolve) => { + wsClient.on('open', resolve); + }); + }; + + const getLastMessage = () => { + return JSON.parse(messages[messages.length - 1]); + }; + + beforeEach(async () => { + messages = []; + port = await startServerWithRandomPort(gateway.startHttpServer.bind(gateway)); + }); + + afterEach(async () => { + console.log(messages); + + wsClient.close(); + + await new Promise((resolve) => { + wsClient.on('close', resolve); + }); + + await waitSecond(); + }); + + it('should receive app error message', async () => { + await connectClient(port); + + await waitSecond(); + + // should receive two messages + + // first message is app initializing + const firstMessage = messages[0]; + expect(JSON.parse(firstMessage)).toMatchObject({ + type: 'maintaining', + payload: { + code: 'APP_INITIALIZING', + message: 'application main is initializing', + }, + }); + + //second message is app not found + expect(getLastMessage()).toMatchObject({ + type: 'maintaining', + payload: { message: 'application main not found', code: 'APP_NOT_FOUND' }, + }); + + const app = new Application({ + database: { + dialect: 'sqlite', + storage: ':memory:', + }, + }); + + await waitSecond(); + + expect(getLastMessage()).toMatchObject({ + type: 'maintaining', + payload: { + code: 'APP_INITIALIZED', + }, + }); + + await app.runAsCLI(['start'], { from: 'user' }); + await waitSecond(); + + expect(getLastMessage()).toMatchObject({ + type: 'maintaining', + payload: { + code: 'APP_NOT_INSTALLED_ERROR', + message: errors.APP_ERROR.message({ app }), + command: { + name: 'start', + }, + }, + }); + + const jestFn = jest.fn(); + + app.on('beforeInstall', async () => { + jestFn(); + }); + + const runningJest = jest.fn(); + app.on('maintaining', ({ status }) => { + if (status === 'command_running') { + runningJest(); + } + }); + + await app.runAsCLI(['install'], { from: 'user' }); + + expect(jestFn).toBeCalledTimes(1); + expect(runningJest).toBeCalledTimes(1); + + await waitSecond(); + expect(getLastMessage()).toMatchObject({ + type: 'maintaining', + payload: { + code: 'APP_RUNNING', + }, + }); + }); + + it('should receive app running message when command end', async () => { + await connectClient(port); + const app = new Application({ + database: { + dialect: 'sqlite', + storage: ':memory:', + }, + }); + + await waitSecond(); + + await app.runCommand('start'); + await app.runCommand('install'); + await app.runCommand('db:auth'); + + await waitSecond(); + + expect(getLastMessage()).toMatchObject({ + type: 'maintaining', + payload: { + code: 'APP_RUNNING', + }, + }); + }); + + it('should receive app stopped when stop app', async () => { + await connectClient(port); + const app = new Application({ + database: { + dialect: 'sqlite', + storage: ':memory:', + }, + }); + + await waitSecond(); + + await app.runCommand('start'); + await app.runCommand('install'); + await waitSecond(); + + expect(getLastMessage()).toMatchObject({ + type: 'maintaining', + payload: { + code: 'APP_RUNNING', + }, + }); + + await app.runCommand('stop'); + + await waitSecond(); + expect(getLastMessage()).toMatchObject({ + type: 'maintaining', + payload: { + code: 'APP_STOPPED', + }, + }); + }); + }); +}); diff --git a/packages/core/server/src/__tests__/i18next.test.ts b/packages/core/server/src/__tests__/i18next.test.ts index 58c80b3e4..0b059b760 100644 --- a/packages/core/server/src/__tests__/i18next.test.ts +++ b/packages/core/server/src/__tests__/i18next.test.ts @@ -29,7 +29,7 @@ describe('i18next', () => { }); afterEach(async () => { - return app.db.close(); + return app.destroy(); }); it('global', async () => { diff --git a/packages/core/server/src/__tests__/life-cycle.test.ts b/packages/core/server/src/__tests__/life-cycle.test.ts index ceb6e556d..e562263e2 100644 --- a/packages/core/server/src/__tests__/life-cycle.test.ts +++ b/packages/core/server/src/__tests__/life-cycle.test.ts @@ -2,14 +2,22 @@ import Application from '../application'; import { Plugin } from '../plugin'; describe('application life cycle', () => { - it('should start application', async () => { - const app = new Application({ + let app: Application; + + beforeEach(async () => { + app = new Application({ database: { dialect: 'sqlite', storage: ':memory:', }, }); + }); + afterEach(async () => { + await app.destroy(); + }); + + it('should start application', async () => { const loadFn = jest.fn(); const installFn = jest.fn(); @@ -35,19 +43,4 @@ describe('application life cycle', () => { await app.install(); expect(installFn).toHaveBeenCalledTimes(1); }); - - it('should listen application', async () => { - const app = new Application({ - database: { - dialect: 'sqlite', - storage: ':memory:', - }, - }); - - await app.start({ listen: { port: 13090 } }); - expect(app.listenServer).not.toBeNull(); - - await app.stop(); - expect(app.listenServer).toBeNull(); - }); }); diff --git a/packages/core/server/src/__tests__/multiple-application.test.ts b/packages/core/server/src/__tests__/multiple-application.test.ts index d51caef38..c2e683bb2 100644 --- a/packages/core/server/src/__tests__/multiple-application.test.ts +++ b/packages/core/server/src/__tests__/multiple-application.test.ts @@ -1,55 +1,8 @@ +import { AppSupervisor } from '@nocobase/server'; import { mockServer, MockServer } from '@nocobase/test'; import { uid } from '@nocobase/utils'; -import { IncomingMessage } from 'http'; -import * as url from 'url'; import Application from '../application'; -describe('multiple apps', () => { - it('should emit beforeGetApplication event', async () => { - const beforeGetApplicationFn = jest.fn(); - - const app = mockServer(); - - app.on('beforeGetApplication', beforeGetApplicationFn); - - app.appManager.addSubApp( - new Application({ - database: app.db, - name: 'sub1', - }), - ); - - app.appManager.setAppSelector(() => 'sub1'); - - await app.agent().resource('test').test({}); - - await app.agent().resource('test').test({}); - - expect(beforeGetApplicationFn).toHaveBeenCalledTimes(2); - - await app.destroy(); - }); - - it('should listen stop event', async () => { - const app = mockServer(); - - const subApp1 = app.appManager.addSubApp( - new Application({ - database: app.db, - name: 'sub1', - }), - ); - - const subApp1StopFn = jest.fn(); - - subApp1.on('afterStop', subApp1StopFn); - - await app.stop(); - - expect(subApp1StopFn).toBeCalledTimes(1); - }); -}); - describe('multiple application', () => { let app: MockServer; beforeEach(async () => { @@ -67,13 +20,11 @@ describe('multiple application', () => { const sub2 = `a_${uid()}`; const sub3 = `a_${uid()}`; - const subApp1 = app.appManager.addSubApp( - new Application({ - database: app.db, - acl: false, - name: sub1, - }), - ); + const subApp1 = new Application({ + database: app.db, + acl: false, + name: sub1, + }); subApp1.resourcer.define({ name: 'test', @@ -84,13 +35,11 @@ describe('multiple application', () => { }, }); - const subApp2 = app.appManager.addSubApp( - new Application({ - database: app.db, - acl: false, - name: sub2, - }), - ); + const subApp2 = new Application({ + database: app.db, + acl: false, + name: sub2, + }); subApp2.resourcer.define({ name: 'test', @@ -101,28 +50,8 @@ describe('multiple application', () => { }, }); - let response = await app.agent().resource('test').test(); - expect(response.statusCode).toEqual(404); - - app.appManager.setAppSelector((req: IncomingMessage) => { - const queryObject = url.parse(req.url, true).query; - return queryObject['app'] as string; - }); - - response = await app.agent().resource('test').test({ - app: sub1, - }); - - expect(response.statusCode).toEqual(200); - - response = await app.agent().resource('test').test({ - app: sub2, - }); - expect(response.statusCode).toEqual(200); - - response = await app.agent().resource('test').test({ - app: sub3, - }); - expect(response.statusCode).toEqual(404); + expect(AppSupervisor.getInstance().hasApp(sub1)).toBeTruthy(); + expect(AppSupervisor.getInstance().hasApp(sub2)).toBeTruthy(); + expect(AppSupervisor.getInstance().hasApp(sub3)).toBeFalsy(); }); }); diff --git a/packages/core/server/src/__tests__/plugin.test.ts b/packages/core/server/src/__tests__/plugin.test.ts index 534d8a0c0..1b72bbcd0 100644 --- a/packages/core/server/src/__tests__/plugin.test.ts +++ b/packages/core/server/src/__tests__/plugin.test.ts @@ -1,9 +1,8 @@ -import { Application } from '../application'; +import { mockServer, MockServer } from '@nocobase/test'; import { Plugin } from '../plugin'; import Plugin1 from './plugins/plugin1'; import Plugin2 from './plugins/plugin2'; import Plugin3 from './plugins/plugin3'; -import { mockServer, MockServer } from '@nocobase/test'; describe('plugin', () => { let app: MockServer; @@ -19,57 +18,6 @@ describe('plugin', () => { await app.destroy(); }); - describe('define', () => { - it('should add plugin with options', async () => { - class MyPlugin extends Plugin { - getName(): string { - return 'test'; - } - } - - const plugin = app.plugin(MyPlugin, { - test: 'hello', - }); - - expect(plugin.options['test']).toEqual('hello'); - }); - - it('plugin name', async () => { - interface Options { - a?: string; - } - class MyPlugin extends Plugin { - async load() { - this.options.a; - } - - getName(): string { - return 'MyPlugin'; - } - } - const plugin = app.plugin(MyPlugin, { - a: 'aa', - }); - plugin.setOptions({ - a: 'a', - }); - expect(plugin).toBeInstanceOf(MyPlugin); - expect(plugin.getName()).toBe('MyPlugin'); - }); - - it('plugin name', async () => { - const plugin = app.plugin(Plugin1); - expect(plugin).toBeInstanceOf(Plugin); - expect(plugin.getName()).toBe('Plugin1'); - }); - - it('plugin name', async () => { - const plugin = app.plugin(Plugin3); - expect(plugin).toBeInstanceOf(Plugin3); - expect(plugin.getName()).toBe('Plugin3'); - }); - }); - describe('load', () => { it('plugin load', async () => { app.plugin( @@ -112,7 +60,7 @@ describe('plugin', () => { }); }); - describe('enable', function () { + describe.skip('enable', function () { it('should call beforeEnable', async () => { const beforeEnable = jest.fn(); diff --git a/packages/core/server/src/__tests__/pm.test.ts b/packages/core/server/src/__tests__/pm.test.ts new file mode 100644 index 000000000..53ad9a2ce --- /dev/null +++ b/packages/core/server/src/__tests__/pm.test.ts @@ -0,0 +1,402 @@ +import { mockDatabase } from '@nocobase/database'; +import Application, { ApplicationOptions } from '../application'; +import Plugin from '../plugin'; +import { PluginManager } from '../plugin-manager'; + +const mockServer = (options?: ApplicationOptions) => { + return new Application({ + database: mockDatabase(), + acl: false, + ...options, + }); +}; + +describe('pm', () => { + let app: Application; + + afterEach(async () => { + if (app) { + await app.destroy(); + } + }); + + test('addPreset', async () => { + class Plugin1 extends Plugin {} + app = mockServer({ + plugins: [[Plugin1, { name: 'plugin1' }]], + }); + await app.load(); + expect(app.pm.get('plugin1').enabled).toBeTruthy(); + expect(app.pm.get(Plugin1).enabled).toBeTruthy(); + expect(app.pm.get('plugin1').options.isPreset).toBeTruthy(); + expect(app.pm.get(Plugin1).options.isPreset).toBeTruthy(); + }); + + test('addPreset', async () => { + class Plugin1 extends Plugin {} + app = mockServer({ + plugins: [Plugin1], + }); + await app.load(); + expect(app.pm.get(Plugin1).enabled).toBeTruthy(); + expect(app.pm.get(Plugin1).options.isPreset).toBeTruthy(); + }); + + test('add', async () => { + class Plugin1 extends Plugin {} + app = mockServer(); + await app.pm.add(Plugin1); + expect(app.pm.get(Plugin1).enabled).toBeFalsy(); + expect(app.pm.get(Plugin1).options.isPreset).toBeFalsy(); + }); + + test('add', async () => { + class Plugin1 extends Plugin {} + app = mockServer(); + await app.pm.add(Plugin1, { enabled: true }); + expect(app.pm.get(Plugin1).enabled).toBeTruthy(); + }); + + test('add', async () => { + class Plugin1 extends Plugin {} + app = mockServer(); + await app.pm.add(Plugin1, { name: 'plugin1', enabled: true }); + expect(app.pm.get(Plugin1).enabled).toBeTruthy(); + expect(app.pm.get('plugin1').enabled).toBeTruthy(); + expect(app.pm.get(Plugin1).options.isPreset).toBeFalsy(); + expect(app.pm.get('plugin1').options.isPreset).toBeFalsy(); + }); + + test('load', async () => { + const arr = []; + class Plugin1 extends Plugin { + async afterAdd() { + arr.push(1); + } + async beforeLoad() { + arr.push(2); + } + async load() { + arr.push(3); + } + } + class Plugin2 extends Plugin { + async afterAdd() { + arr.push(4); + await app.pm.add(Plugin3, { enabled: true }); + } + async beforeLoad() { + arr.push(5); + } + async load() { + arr.push(6); + } + } + class Plugin3 extends Plugin { + async afterAdd() { + arr.push(7); + } + async beforeLoad() { + arr.push(8); + } + async load() { + arr.push(9); + } + } + app = mockServer({ + plugins: [Plugin1, Plugin2], + }); + await app.pm.initPlugins(); + await app.pm.load(); + await app.pm.load(); + expect(arr).toEqual([1, 4, 7, 2, 5, 8, 3, 6, 9]); + }); + + test('install', async () => { + const arr = []; + class Plugin1 extends Plugin { + async install() { + arr.push(1); + } + } + class Plugin2 extends Plugin { + async install() { + arr.push(2); + await app.pm.add(Plugin3, { enabled: true }); + await app.pm.install(); + } + } + class Plugin3 extends Plugin { + async install() { + arr.push(3); + } + } + app = mockServer({ + plugins: [Plugin1, Plugin2], + }); + await app.load(); + await app.pm.install(); + await app.pm.install(); + expect(arr).toEqual([1, 2, 3]); + }); + + test('enable', async () => { + app = mockServer(); + await app.load(); + await expect(() => app.pm.enable('Plugin0')).rejects.toThrow('Plugin0 plugin does not exist'); + }); + + test('enable', async () => { + const loadFn = jest.fn(); + class Plugin1 extends Plugin { + async beforeEnable() { + loadFn(); + } + async install() { + loadFn(); + } + async afterEnable() { + loadFn(); + } + } + app = mockServer({ + plugins: [[Plugin1, { name: 'Plugin1' }]], + }); + await app.load(); + await app.pm.enable('Plugin1'); + expect(loadFn).not.toBeCalled(); + }); + + test('enable', async () => { + const resolvePlugin = PluginManager.resolvePlugin; + PluginManager.resolvePlugin = (pluginName) => { + return Plugin1; + }; + const loadFn = jest.fn(); + class Plugin1 extends Plugin { + async beforeEnable() { + loadFn(); + } + async install() { + loadFn(); + } + async afterEnable() { + loadFn(); + } + } + app = mockServer(); + await app.load(); + await app.install(); + await app.pm.repository.create({ + values: { + name: 'Plugin1', + enabled: true, + installed: true, + }, + }); + await app.reload(); + expect(app.pm.get('Plugin1').enabled).toBeTruthy(); + expect(app.pm.get('Plugin1').installed).toBeTruthy(); + await app.pm.enable('Plugin1'); + expect(loadFn).not.toBeCalled(); + PluginManager.resolvePlugin = resolvePlugin; + }); + + test('enable', async () => { + const resolvePlugin = PluginManager.resolvePlugin; + PluginManager.resolvePlugin = (pluginName) => { + return Plugin1; + }; + const loadFn = jest.fn(); + class Plugin1 extends Plugin { + async beforeEnable() { + loadFn(); + } + async install() { + loadFn(); + } + async afterEnable() { + loadFn(); + } + } + app = mockServer(); + await app.load(); + await app.install(); + await app.pm.repository.create({ + values: { + name: 'Plugin1', + }, + }); + await app.reload(); + await app.pm.enable('Plugin1'); + expect(app.pm.get('Plugin1').enabled).toBeTruthy(); + expect(app.pm.get('Plugin1').installed).toBeTruthy(); + expect(loadFn).toBeCalled(); + expect(loadFn).toBeCalledTimes(3); + await app.pm.enable('Plugin1'); + expect(loadFn).toBeCalledTimes(3); + await app.pm.disable('Plugin1'); + await app.pm.enable('Plugin1'); + expect(loadFn).toBeCalledTimes(5); + PluginManager.resolvePlugin = resolvePlugin; + }); + + test('enable', async () => { + const loadFn = jest.fn(); + class Plugin1 extends Plugin { + async beforeEnable() { + loadFn(); + } + async install() { + loadFn(); + } + async afterEnable() { + loadFn(); + } + } + class Plugin2 extends Plugin { + async beforeEnable() { + loadFn(); + } + async install() { + loadFn(); + } + async afterEnable() { + loadFn(); + } + } + const resolvePlugin = PluginManager.resolvePlugin; + PluginManager.resolvePlugin = (pluginName: string) => { + return { + Plugin1, + Plugin2, + }[pluginName]; + }; + app = mockServer(); + await app.load(); + await app.install(); + await app.pm.repository.create({ + values: [ + { + name: 'Plugin1', + }, + { + name: 'Plugin2', + }, + ], + }); + await app.reload(); + await app.pm.enable(['Plugin1', 'Plugin2']); + expect(app.pm.get('Plugin1').enabled).toBeTruthy(); + expect(app.pm.get('Plugin1').installed).toBeTruthy(); + expect(app.pm.get('Plugin2').enabled).toBeTruthy(); + expect(app.pm.get('Plugin2').installed).toBeTruthy(); + expect(loadFn).toBeCalled(); + expect(loadFn).toBeCalledTimes(6); + await app.pm.enable(['Plugin1', 'Plugin2']); + expect(loadFn).toBeCalledTimes(6); + PluginManager.resolvePlugin = resolvePlugin; + }); + + test('disable', async () => { + const resolvePlugin = PluginManager.resolvePlugin; + PluginManager.resolvePlugin = (pluginName) => { + return Plugin1; + }; + const loadFn = jest.fn(); + class Plugin1 extends Plugin { + async beforeDisable() { + loadFn(); + } + async afterDisable() { + loadFn(); + } + } + app = mockServer(); + await app.load(); + await app.install(); + await app.pm.repository.create({ + values: { + name: 'Plugin1', + }, + }); + await app.reload(); + expect(app.pm.get('Plugin1').enabled).toBeFalsy(); + expect(app.pm.get('Plugin1').installed).toBeFalsy(); + await app.pm.disable('Plugin1'); + expect(loadFn).not.toBeCalled(); + PluginManager.resolvePlugin = resolvePlugin; + }); + + test('disable', async () => { + const resolvePlugin = PluginManager.resolvePlugin; + PluginManager.resolvePlugin = (pluginName) => { + return Plugin1; + }; + const loadFn = jest.fn(); + class Plugin1 extends Plugin { + async beforeDisable() { + loadFn(); + } + async afterDisable() { + loadFn(); + } + } + app = mockServer(); + await app.load(); + await app.install(); + await app.pm.repository.create({ + values: { + name: 'Plugin1', + enabled: true, + installed: true, + }, + }); + await app.reload(); + expect(app.pm.get('Plugin1').enabled).toBeTruthy(); + expect(app.pm.get('Plugin1').installed).toBeTruthy(); + await app.pm.disable('Plugin1'); + expect(loadFn).toBeCalled(); + expect(loadFn).toBeCalledTimes(2); + expect(app.pm.get('Plugin1').enabled).toBeFalsy(); + expect(app.pm.get('Plugin1').installed).toBeTruthy(); + PluginManager.resolvePlugin = resolvePlugin; + }); + + test('install', async () => { + class Plugin0 extends Plugin { + async install() { + await this.app.pm.repository.create({ + values: { + name: 'Plugin1', + enabled: true, + }, + }); + await this.app.pm.repository.init(); + await this.app.pm.load(); + await this.app.pm.install(); + } + } + class Plugin1 extends Plugin {} + class Plugin2 extends Plugin {} + const resolvePlugin = PluginManager.resolvePlugin; + PluginManager.resolvePlugin = (pluginName: string) => { + return { + Plugin0, + Plugin1, + Plugin2, + }[pluginName]; + }; + app = mockServer({ + plugins: ['Plugin0'], + }); + await app.load(); + await app.install(); + expect(app.pm.get('Plugin1').enabled).toBeTruthy(); + expect(app.pm.get('Plugin1').installed).toBeTruthy(); + const record = await app.pm.repository.findOne({ filter: { name: 'Plugin1' } }); + expect(record.enabled).toBeTruthy(); + expect(record.installed).toBeTruthy(); + PluginManager.resolvePlugin = resolvePlugin; + }); +}); diff --git a/packages/core/server/src/app-manager.ts b/packages/core/server/src/app-manager.ts deleted file mode 100644 index dabe30bea..000000000 --- a/packages/core/server/src/app-manager.ts +++ /dev/null @@ -1,112 +0,0 @@ -import http, { IncomingMessage, ServerResponse } from 'http'; -import Application from './application'; - -type AppSelectorReturn = Application | string | undefined | null; - -type AppSelector = (req: IncomingMessage) => AppSelectorReturn | Promise; - -export class AppManager { - public applications: Map = new Map(); - public app: Application; - public runningMode: 'single' | 'multiple' = 'multiple'; - public singleAppName: string | null = null; - - constructor(app: Application) { - this.bindMainApplication(app); - - // if STARTUP_SUBAPP is set, run in single mode - if (process.env.STARTUP_SUBAPP) { - this.singleAppName = process.env.STARTUP_SUBAPP; - this.runningMode = 'single'; - } - } - - bindMainApplication(mainApp: Application) { - this.app = mainApp; - const passEventToSubApps = (eventName, method) => { - mainApp.on(eventName, async (mainApp, options) => { - console.log(`receive event ${eventName} from ${mainApp.name}`); - for (const application of this.applications.values()) { - console.log(`pass ${eventName} to ${application.name} `); - await application[method](options); - } - }); - }; - - passEventToSubApps('beforeDestroy', 'destroy'); - passEventToSubApps('beforeStop', 'stop'); - } - - appSelector: AppSelector = async (req: IncomingMessage) => this.app; - - addSubApp(application): Application { - this.applications.set(application.name, application); - this.app.emit('afterSubAppAdded', application); - return application; - } - - async removeApplication(name: string) { - const application = this.applications.get(name); - if (!application) { - return; - } - - await application.destroy(); - - console.log(`remove application ${name}`); - this.applications.delete(name); - } - - setAppSelector(selector: AppSelector) { - this.appSelector = selector; - } - - listen(...args) { - const server = http.createServer(this.callback()); - return server.listen(...args); - } - - async getApplication(appName: string, options = {}): Promise { - await this.app.emitAsync('beforeGetApplication', { - appManager: this, - name: appName, - options, - }); - - return this.applications.get(appName); - } - - callback() { - return async (req: IncomingMessage, res: ServerResponse) => { - const appManager = this.app.appManager; - - let handleApp; - if (this.runningMode === 'single') { - handleApp = this.singleAppName; - } else { - handleApp = (await appManager.appSelector(req)) || appManager.app; - } - - if (typeof handleApp === 'string') { - handleApp = await appManager.getApplication(handleApp); - if (!handleApp) { - res.statusCode = 404; - return res.end( - JSON.stringify({ - redirectTo: process.env.APP_NOT_FOUND_REDIRECT_TO, - errors: [ - { - message: 'Application Not Found', - }, - ], - }), - ); - } - - if (handleApp.stopped) await handleApp.start(); - } - - handleApp.callback()(req, res); - }; - } -} diff --git a/packages/core/server/src/app-supervisor.ts b/packages/core/server/src/app-supervisor.ts new file mode 100644 index 000000000..b958427d7 --- /dev/null +++ b/packages/core/server/src/app-supervisor.ts @@ -0,0 +1,306 @@ +import { applyMixins, AsyncEmitter } from '@nocobase/utils'; +import { Mutex } from 'async-mutex'; +import { EventEmitter } from 'events'; +import Application, { ApplicationOptions, MaintainingCommandStatus } from './application'; + +type BootOptions = { + appName: string; + options: any; + appSupervisor: AppSupervisor; +}; + +type AppBootstrapper = (bootOptions: BootOptions) => Promise; + +type AppStatus = 'initializing' | 'initialized' | 'running' | 'commanding' | 'stopped' | 'error' | 'not_found'; + +export class AppSupervisor extends EventEmitter implements AsyncEmitter { + private static instance: AppSupervisor; + public runningMode: 'single' | 'multiple' = 'multiple'; + public singleAppName: string | null = null; + declare emitAsync: (event: string | symbol, ...args: any[]) => Promise; + public apps: { + [appName: string]: Application; + } = {}; + + public appErrors: { + [appName: string]: Error; + } = {}; + + public appStatus: { + [appName: string]: AppStatus; + } = {}; + + public lastMaintainingMessage: { + [appName: string]: string; + } = {}; + + public statusBeforeCommanding: { + [appName: string]: AppStatus; + } = {}; + + private appMutexes = {}; + private appBootstrapper: AppBootstrapper = null; + + private constructor() { + super(); + + if (process.env.STARTUP_SUBAPP) { + this.runningMode = 'single'; + this.singleAppName = process.env.STARTUP_SUBAPP; + } + } + + public static getInstance(): AppSupervisor { + if (!AppSupervisor.instance) { + AppSupervisor.instance = new AppSupervisor(); + } + + return AppSupervisor.instance; + } + + setAppError(appName: string, error: Error) { + this.appErrors[appName] = error; + + this.emit('appError', { + appName: appName, + error, + }); + } + + hasAppError(appName: string) { + return !!this.appErrors[appName]; + } + + clearAppError(appName: string) { + delete this.appErrors[appName]; + } + + async reset() { + const appNames = Object.keys(this.apps); + for (const appName of appNames) { + await this.removeApp(appName); + } + + this.appBootstrapper = null; + this.removeAllListeners(); + } + + async destroy() { + await this.reset(); + AppSupervisor.instance = null; + } + + setAppStatus(appName: string, status: AppStatus) { + if (this.appStatus[appName] === status) { + return; + } + + this.appStatus[appName] = status; + + this.emit('appStatusChanged', { + appName, + status, + }); + } + + getMutexOfApp(appName: string) { + if (!this.appMutexes[appName]) { + this.appMutexes[appName] = new Mutex(); + } + + return this.appMutexes[appName]; + } + + async bootStrapApp(appName: string, options = {}) { + await this.getMutexOfApp(appName).runExclusive(async () => { + if (!this.hasApp(appName)) { + this.setAppStatus(appName, 'initializing'); + + if (this.appBootstrapper) { + await this.appBootstrapper({ + appSupervisor: this, + appName, + options, + }); + } + + if (!this.hasApp(appName)) { + this.setAppStatus(appName, 'not_found'); + } else if (!this.getAppStatus(appName) || this.getAppStatus(appName) == 'initializing') { + this.setAppStatus(appName, 'initialized'); + } + } + }); + } + + async getApp( + appName: string, + options: { + withOutBootStrap?: boolean; + [key: string]: any; + } = {}, + ) { + if (!options.withOutBootStrap) { + await this.bootStrapApp(appName, options); + } + + return this.apps[appName]; + } + + setAppBootstrapper(appBootstrapper: AppBootstrapper) { + this.appBootstrapper = appBootstrapper; + } + + getAppStatus(appName: string, defaultStatus?: AppStatus): AppStatus | null { + const status = this.appStatus[appName]; + + if (status === undefined && defaultStatus !== undefined) { + return defaultStatus; + } + + return status; + } + + bootMainApp(options: ApplicationOptions) { + return new Application(options); + } + + hasApp(appName: string): boolean { + return !!this.apps[appName]; + } + + // add app into supervisor + addApp(app: Application) { + // if there is already an app with the same name, throw error + if (this.apps[app.name]) { + throw new Error(`app ${app.name} already exists`); + } + + console.log(`add app ${app.name} into supervisor`); + + this.bindAppEvents(app); + + this.apps[app.name] = app; + + this.emit('afterAppAdded', app); + + if (!this.getAppStatus(app.name) || this.getAppStatus(app.name) == 'not_found') { + this.setAppStatus(app.name, 'initialized'); + } + + return app; + } + + // get registered app names + async getAppsNames() { + const apps = Object.values(this.apps); + + return apps.map((app) => app.name); + } + + async removeApp(appName: string) { + if (!this.apps[appName]) { + console.log(`app ${appName} not exists`); + return; + } + + // call app.destroy + await this.apps[appName].runCommand('destroy'); + } + + subApps() { + return Object.values(this.apps).filter((app) => app.name !== 'main'); + } + + on(eventName: string | symbol, listener: (...args: any[]) => void): this { + const listeners = this.listeners(eventName); + const listenerName = listener.name; + + if (listenerName !== '') { + const exists = listeners.find((l) => l.name === listenerName); + + if (exists) { + super.removeListener(eventName, exists as any); + } + } + + return super.on(eventName, listener); + } + + private bindAppEvents(app: Application) { + app.on('afterDestroy', () => { + delete this.apps[app.name]; + delete this.appStatus[app.name]; + delete this.appErrors[app.name]; + delete this.lastMaintainingMessage[app.name]; + delete this.statusBeforeCommanding[app.name]; + }); + + app.on('maintainingMessageChanged', ({ message, maintainingStatus }) => { + if (this.lastMaintainingMessage[app.name] === message) { + return; + } + + this.lastMaintainingMessage[app.name] = message; + + const appStatus = this.getAppStatus(app.name); + + if (!maintainingStatus && appStatus !== 'running') { + return; + } + + this.emit('appMaintainingMessageChanged', { + appName: app.name, + message, + status: appStatus, + command: appStatus == 'running' ? null : maintainingStatus.command, + }); + }); + + app.on('__started', async () => { + this.setAppStatus(app.name, 'running'); + }); + + app.on('afterStop', async () => { + this.setAppStatus(app.name, 'stopped'); + }); + + app.on('maintaining', (maintainingStatus: MaintainingCommandStatus) => { + const { status } = maintainingStatus; + + switch (status) { + case 'command_begin': + { + this.statusBeforeCommanding[app.name] = this.getAppStatus(app.name); + this.setAppStatus(app.name, 'commanding'); + } + break; + + case 'command_running': + // emit status changed + // this.emit('appMaintainingStatusChanged', maintainingStatus); + break; + case 'command_end': + { + const appStatus = this.getAppStatus(app.name); + // emit status changed + this.emit('appMaintainingStatusChanged', maintainingStatus); + + // not change + if (appStatus == 'commanding') { + this.setAppStatus(app.name, this.statusBeforeCommanding[app.name]); + } + } + break; + case 'command_error': + { + this.setAppError(app.name, maintainingStatus.error); + this.setAppStatus(app.name, 'error'); + } + break; + } + }); + } +} + +applyMixins(AppSupervisor, [AsyncEmitter]); diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 12e198fba..86542aaf3 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -2,29 +2,31 @@ import { ACL } from '@nocobase/acl'; import { registerActions } from '@nocobase/actions'; import { actions as authActions, AuthManager } from '@nocobase/auth'; import { Cache, createCache, ICacheConfig } from '@nocobase/cache'; -import Database, { Collection, CollectionOptions, IDatabaseOptions } from '@nocobase/database'; +import Database, { CollectionOptions, IDatabaseOptions } from '@nocobase/database'; import { AppLoggerOptions, createAppLogger, Logger } from '@nocobase/logger'; import Resourcer, { ResourceOptions } from '@nocobase/resourcer'; import { applyMixins, AsyncEmitter, Toposort, ToposortOptions } from '@nocobase/utils'; import chalk from 'chalk'; import { Command, CommandOptions, ParseOptions } from 'commander'; -import { Server } from 'http'; +import { IncomingMessage, Server, ServerResponse } from 'http'; import { i18n, InitOptions } from 'i18next'; import Koa, { DefaultContext as KoaDefaultContext, DefaultState as KoaDefaultState } from 'koa'; import compose from 'koa-compose'; -import semver from 'semver'; -import { promisify } from 'util'; +import lodash from 'lodash'; import { createACL } from './acl'; -import { AppManager } from './app-manager'; +import { AppSupervisor } from './app-supervisor'; import { registerCli } from './commands'; -import { createI18n, createResourcer, registerMiddlewares } from './helper'; +import { ApplicationNotInstall } from './errors/application-not-install'; +import { createAppProxy, createI18n, createResourcer, getCommandFullName, registerMiddlewares } from './helper'; +import { ApplicationVersion } from './helpers/application-version'; import { Locale } from './locale'; import { Plugin } from './plugin'; import { InstallOptions, PluginManager } from './plugin-manager'; const packageJson = require('../package.json'); -export type PluginConfiguration = string | [string, any]; +export type PluginType = string | typeof Plugin; +export type PluginConfiguration = PluginType | [PluginType, any]; export interface ResourcerOptions { prefix?: string; @@ -61,14 +63,6 @@ export interface DefaultContext extends KoaDefaultContext { [key: string]: any; } -interface MiddlewareOptions { - name?: string; - resourceName?: string; - resourceNames?: string[]; - insertBefore?: string; - insertAfter?: string; -} - interface ActionsOptions { resourceName?: string; resourceNames?: string[]; @@ -92,251 +86,169 @@ interface ListenOptions { interface StartOptions { cliArgs?: any[]; dbSync?: boolean; - listen?: ListenOptions; + checkInstall?: boolean; } -export class ApplicationVersion { - protected app: Application; - protected collection: Collection; +type MaintainingStatus = 'command_begin' | 'command_end' | 'command_running' | 'command_error'; - constructor(app: Application) { - this.app = app; - if (!app.db.hasCollection('applicationVersion')) { - app.db.collection({ - name: 'applicationVersion', - namespace: 'core.applicationVersion', - duplicator: 'required', - timestamps: false, - fields: [{ name: 'value', type: 'string' }], - }); - } - this.collection = this.app.db.getCollection('applicationVersion'); - } - - async get() { - if (await this.app.db.collectionExistsInDb('applicationVersion')) { - const model = await this.collection.model.findOne(); - if (!model) { - return null; - } - return model.get('value') as any; - } - return null; - } - - async update() { - await this.collection.sync(); - await this.collection.model.destroy({ - truncate: true, - }); - - await this.collection.model.create({ - value: this.app.getVersion(), - }); - } - - async satisfies(range: string) { - if (await this.app.db.collectionExistsInDb('applicationVersion')) { - const model: any = await this.collection.model.findOne(); - const version = model?.value as any; - if (!version) { - return true; - } - return semver.satisfies(version, range, { includePrerelease: true }); - } - return true; - } -} +export type MaintainingCommandStatus = { + command: { + name: string; + }; + status: MaintainingStatus; + error?: Error; +}; export class Application extends Koa implements AsyncEmitter { - protected _db: Database; - protected _logger: Logger; - - protected _resourcer: Resourcer; - - protected _cache: Cache; - - protected _cli: Command; - - protected _i18n: i18n; - - protected _pm: PluginManager; - - protected _acl: ACL; - - protected _appManager: AppManager; - - protected _authManager: AuthManager; - - protected _locales: Locale; - - protected _version: ApplicationVersion; - - protected plugins = new Map(); - public listenServer: Server; - declare middleware: any; - stopped = false; + ready = false; + declare emitAsync: (event: string | symbol, ...args: any[]) => Promise; + public rawOptions: ApplicationOptions; + public activatedCommand: { + name: string; + } = null; + public running = false; + protected plugins = new Map(); + protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance(); + protected _started: boolean; + private _authenticated = false; + private _maintaining = false; + private _maintainingCommandStatus: MaintainingCommandStatus; + private _maintainingStatusBeforeCommand: MaintainingCommandStatus | null; constructor(public options: ApplicationOptions) { super(); + this.rawOptions = this.name == 'main' ? lodash.cloneDeep(options) : {}; this.init(); + + this._appSupervisor.addApp(this); } + protected _loaded: boolean; + + get loaded() { + return this._loaded; + } + + private _maintainingMessage: string; + + get maintainingMessage() { + return this._maintainingMessage; + } + + protected _db: Database; + get db() { return this._db; } - get cache() { - return this._cache; + protected _logger: Logger; + + get logger() { + return this._logger; } + protected _resourcer: Resourcer; + get resourcer() { return this._resourcer; } + protected _cache: Cache; + + get cache() { + return this._cache; + } + + protected _cli: Command; + get cli() { return this._cli; } - get acl() { - return this._acl; - } + protected _i18n: i18n; get i18n() { return this._i18n; } + protected _pm: PluginManager; + get pm() { return this._pm; } - get version() { - return this._version; + protected _acl: ACL; + + get acl() { + return this._acl; } - get appManager() { - return this._appManager; - } + protected _authManager: AuthManager; get authManager() { return this._authManager; } - get logger() { - return this._logger; + protected _locales: Locale; + + get locales() { + return this._locales; + } + + protected _version: ApplicationVersion; + + get version() { + return this._version; } get log() { return this._logger; } - get locales() { - return this._locales; - } - get name() { return this.options.name || 'main'; } - protected init() { - const options = this.options; - - const logger = createAppLogger(options.logger); - this._logger = logger.instance; - - // @ts-ignore - this._events = []; - // @ts-ignore - this._eventsCount = []; - - this.removeAllListeners(); - - this.middleware = new Toposort(); - this.plugins = new Map(); - this._acl = createACL(); - - this.use(logger.middleware, { tag: 'logger' }); - - if (this._db) { - // MaxListenersExceededWarning - this._db.removeAllListeners(); - } - - this._db = this.createDatabase(options); - - this._resourcer = createResourcer(options); - this._cli = new Command('nocobase').usage('[command] [options]'); - this._i18n = createI18n(options); - this._cache = createCache(options.cache); - this.context.db = this._db; - this.context.logger = this._logger; - this.context.resourcer = this._resourcer; - this.context.cache = this._cache; - - if (this._pm) { - this._pm = this._pm.clone(); - } else { - this._pm = new PluginManager({ - app: this, - plugins: options.plugins, - }); - } - - if (this._appManager) { - this._appManager.bindMainApplication(this); - } else { - this._appManager = new AppManager(this); - } - - this._authManager = new AuthManager({ - authKey: 'X-Authenticator', - default: 'basic', - }); - this.resource({ - name: 'auth', - actions: authActions, - }); - this._resourcer.use(this._authManager.middleware(), { tag: 'auth' }); - - if (this.options.acl !== false) { - this._resourcer.use(this._acl.middleware(), { tag: 'acl', after: ['auth'] }); - } - - this._locales = new Locale(this); - - registerMiddlewares(this, options); - - if (options.registerActions !== false) { - registerActions(this); - } - - registerCli(this); - - this._version = new ApplicationVersion(this); + isMaintaining() { + return this._maintaining; } - private createDatabase(options: ApplicationOptions) { - const db = new Database({ - ...(options.database instanceof Database ? options.database.options : options.database), - migrator: { - context: { app: this }, - }, + getMaintaining() { + return this._maintainingCommandStatus; + } + + setMaintaining(_maintainingCommandStatus: MaintainingCommandStatus) { + this._maintainingCommandStatus = _maintainingCommandStatus; + + this.emit('maintaining', _maintainingCommandStatus); + + if (_maintainingCommandStatus.status == 'command_end') { + this._maintaining = false; + return; + } + + this._maintaining = true; + } + + setMaintainingMessage(message: string) { + this._maintainingMessage = message; + + this.emit('maintainingMessageChanged', { + message: this._maintainingMessage, + maintainingStatus: this._maintainingCommandStatus, }); - - db.setLogger(this._logger); - - return db; } getVersion() { return packageJson.version; } - plugin(pluginClass: any, options?: O): Plugin { - return this.pm.addStatic(pluginClass, options); + plugin(pluginClass: any, options?: O) { + this.log.debug(`add plugin ${pluginClass.name}`); + this.pm.addPreset(pluginClass, options); } // @ts-ignore @@ -353,14 +265,12 @@ export class Application exten if (!this.listenerCount('error')) this.on('error', this.onerror); - const handleRequest = (req, res) => { + return (req: IncomingMessage, res: ServerResponse) => { const ctx = this.createContext(req, res); // @ts-ignore return this.handleRequest(ctx, fn); }; - - return handleRequest; } collection(options: CollectionOptions) { @@ -380,32 +290,54 @@ export class Application exten } findCommand(name: string): Command { - return (this.cli)._findCommand(name); + return (this.cli as any)._findCommand(name); } async load(options?: any) { + if (this._loaded) { + return; + } + if (options?.reload) { - console.log(`Reload the ${this.name} application configuration`); + this.setMaintainingMessage('app reload'); + this.log.info(`app.reload()`); const oldDb = this._db; this.init(); await oldDb.close(); } + this.setMaintainingMessage('init plugins'); + await this.pm.initPlugins(); + + this.setMaintainingMessage('start load'); + + this.setMaintainingMessage('emit beforeLoad'); await this.emitAsync('beforeLoad', this, options); + await this.pm.load(options); + + this.setMaintainingMessage('emit afterLoad'); await this.emitAsync('afterLoad', this, options); + this._loaded = true; } async reload(options?: any) { + this.log.debug(`start reload`); + + this._loaded = false; + await this.load({ ...options, reload: true, }); + this.log.debug('emit afterReload'); + this.setMaintainingMessage('emit afterReload'); await this.emitAsync('afterReload', this, options); + this.log.debug(`finish reload`); } - getPlugin

(name: string) { + getPlugin

(name: string | typeof Plugin) { return this.pm.get(name) as P; } @@ -413,75 +345,129 @@ export class Application exten return this.runAsCLI(argv); } - async runAsCLI(argv = process.argv, options?: ParseOptions) { - try { - await this.db.auth({ retry: 30 }); - } catch (error) { - console.log(chalk.red(error.message)); - process.exit(1); + async authenticate() { + if (this._authenticated) { + return; } - + this._authenticated = true; + await this.db.auth({ retry: 30 }); await this.dbVersionCheck({ exit: true }); - await this.db.prepare(); + } - if (argv?.[2] !== 'upgrade') { - await this.load({ - method: argv?.[2], + async runCommand(command: string, ...args: any[]) { + return await this.runAsCLI([command, ...args], { from: 'user' }); + } + + createCli() { + return new Command('nocobase') + .usage('[command] [options]') + .hook('preAction', async (_, actionCommand) => { + this.activatedCommand = { + name: getCommandFullName(actionCommand), + }; + + this.setMaintaining({ + status: 'command_begin', + command: this.activatedCommand, + }); + + this.setMaintaining({ + status: 'command_running', + command: this.activatedCommand, + }); + + await this.authenticate(); + await this.load(); + }) + .hook('postAction', async (_, actionCommand) => { + if (this._maintainingStatusBeforeCommand?.error && this._started) { + await this.restart(); + } }); + } + + async runAsCLI(argv = process.argv, options?: ParseOptions) { + if (this.activatedCommand) { + return; } - return this.cli.parseAsync(argv, options); + this._maintainingStatusBeforeCommand = this._maintainingCommandStatus; + + try { + const command = await this.cli.parseAsync(argv, options); + + this.setMaintaining({ + status: 'command_end', + command: this.activatedCommand, + }); + + return command; + } catch (error) { + console.log(`run command ${this.activatedCommand.name} error:`, error); + this.setMaintaining({ + status: 'command_error', + command: this.activatedCommand, + error, + }); + } finally { + this.activatedCommand = null; + } } async start(options: StartOptions = {}) { + if (this._started) { + return; + } + + this._started = true; + + if (options.checkInstall && !(await this.isInstalled())) { + throw new ApplicationNotInstall( + `Application ${this.name} is not installed, Please run 'yarn nocobase install' command first`, + ); + } + + this.setMaintainingMessage('starting app...'); + if (this.db.closed()) { await this.db.reconnect(); } - if (options.dbSync) { - console.log('db sync...'); - await this.db.sync(); - } - + this.setMaintainingMessage('emit beforeStart'); await this.emitAsync('beforeStart', this, options); - if (options?.listen?.port) { - const pmServer = await this.pm.listen(); - - const listen = () => - new Promise((resolve, reject) => { - const Server = this.listen(options?.listen, () => { - resolve(Server); - }); - - Server.on('error', (err) => { - reject(err); - }); - - Server.on('close', () => { - pmServer.close(); - }); - }); - - try { - //@ts-ignore - this.listenServer = await listen(); - } catch (e) { - console.error(e); - process.exit(1); - } - } - + this.setMaintainingMessage('emit afterStart'); await this.emitAsync('afterStart', this, options); + await this.emitAsync('__started', this, options); this.stopped = false; } - listen(...args): Server { - return this.appManager.listen(...args); + async isStarted() { + return this._started; + } + + async tryReloadOrRestart() { + if (this._started) { + await this.restart(); + } else { + await this.reload(); + } + } + + async restart(options: StartOptions = {}) { + if (!this._started) { + return; + } + this._started = false; + await this.reload(options); + await this.start(options); + this.emit('__restarted', this, options); } async stop(options: any = {}) { + this.log.debug('stop app...'); + this.setMaintainingMessage('stopping app...'); if (this.stopped) { this.log.warn(`Application ${this.name} already stopped`); return; @@ -489,31 +475,33 @@ export class Application exten await this.emitAsync('beforeStop', this, options); - // close http server - if (this.listenServer) { - await promisify(this.listenServer.close).call(this.listenServer); - this.listenServer = null; - } - try { // close database connection // silent if database already closed if (!this.db.closed()) { + this.logger.info(`close db`); await this.db.close(); } } catch (e) { - console.log(e); + this.log.error(e); } await this.emitAsync('afterStop', this, options); this.stopped = true; - console.log(`${this.name} is stopped`); + this.log.info(`${this.name} is stopped`); + this._started = false; } async destroy(options: any = {}) { + this.logger.debug('start destroy app'); + this.setMaintainingMessage('destroying app...'); await this.emitAsync('beforeDestroy', this, options); await this.stop(options); + + this.logger.debug('emit afterDestroy'); await this.emitAsync('afterDestroy', this, options); + + this.logger.debug('finish destroy app'); } async dbVersionCheck(options?: { exit?: boolean }) { @@ -554,19 +542,37 @@ export class Application exten } async install(options: InstallOptions = {}) { - console.log('Database dialect: ' + this.db.sequelize.getDialect()); + this.setMaintainingMessage('installing app...'); + this.log.debug('Database dialect: ' + this.db.sequelize.getDialect()); if (options?.clean || options?.sync?.force) { - console.log('Truncate database and reload app configuration'); + this.log.debug('truncate database'); await this.db.clean({ drop: true }); - await this.reload({ method: 'install' }); + this.log.debug('app reloading'); + await this.reload(); + } else if (await this.isInstalled()) { + this.log.warn('app is installed'); + return; } + this.log.debug('emit beforeInstall'); + this.setMaintainingMessage('call beforeInstall hook...'); await this.emitAsync('beforeInstall', this, options); - await this.db.sync(); + this.log.debug('start install plugins'); await this.pm.install(options); + this.log.debug('update version'); await this.version.update(); + this.log.debug('emit afterInstall'); + this.setMaintainingMessage('call afterInstall hook...'); await this.emitAsync('afterInstall', this, options); + + if (this._maintainingStatusBeforeCommand?.error) { + return; + } + + if (this._started) { + await this.restart(); + } } async upgrade(options: any = {}) { @@ -581,15 +587,113 @@ export class Application exten }); await this.version.update(); await this.emitAsync('afterUpgrade', this, options); + this.log.debug(chalk.green(`✨ NocoBase has been upgraded to v${this.getVersion()}`)); + if (this._started) { + await this.restart(); + } } - declare emitAsync: (event: string | symbol, ...args: any[]) => Promise; - toJSON() { return { appName: this.name, + name: this.name, }; } + + reInitEvents() { + for (const eventName of this.eventNames()) { + for (const listener of this.listeners(eventName)) { + if (listener['_reinitializable']) { + this.removeListener(eventName, listener as any); + } + } + } + } + + protected init() { + const options = this.options; + + const logger = createAppLogger({ + ...options.logger, + defaultMeta: { + app: this.name, + }, + }); + + this._logger = logger.instance; + + this.reInitEvents(); + + this.middleware = new Toposort(); + this.plugins = new Map(); + this._acl = createACL(); + + this.use(logger.middleware, { tag: 'logger' }); + + if (this._db) { + // MaxListenersExceededWarning + this._db.removeAllListeners(); + } + + this._db = this.createDatabase(options); + + this._resourcer = createResourcer(options); + this._cli = this.createCli(); + this._i18n = createI18n(options); + this._cache = createCache(options.cache); + this.context.db = this._db; + this.context.logger = this._logger; + this.context.resourcer = this._resourcer; + this.context.cache = this._cache; + + const plugins = this._pm ? this._pm.options.plugins : options.plugins; + + this._pm = new PluginManager({ + app: this, + plugins: plugins || [], + }); + + this._authManager = new AuthManager({ + authKey: 'X-Authenticator', + default: 'basic', + }); + + this.resource({ + name: 'auth', + actions: authActions, + }); + + this._resourcer.use(this._authManager.middleware(), { tag: 'auth' }); + + if (this.options.acl !== false) { + this._resourcer.use(this._acl.middleware(), { tag: 'acl', after: ['auth'] }); + } + + this._locales = new Locale(createAppProxy(this)); + + registerMiddlewares(this, options); + + if (options.registerActions !== false) { + registerActions(this); + } + + registerCli(this); + + this._version = new ApplicationVersion(this); + } + + private createDatabase(options: ApplicationOptions) { + const db = new Database({ + ...(options.database instanceof Database ? options.database.options : options.database), + migrator: { + context: { app: this }, + }, + }); + + db.setLogger(this._logger); + + return db; + } } applyMixins(Application, [AsyncEmitter]); diff --git a/packages/core/server/src/commands/db-sync.ts b/packages/core/server/src/commands/db-sync.ts index 8e91d9b1c..3c9adf9e8 100644 --- a/packages/core/server/src/commands/db-sync.ts +++ b/packages/core/server/src/commands/db-sync.ts @@ -11,8 +11,5 @@ export default (app: Application) => { drop: force, }, }); - await app.stop({ - cliArgs, - }); }); }; diff --git a/packages/core/server/src/commands/destroy.ts b/packages/core/server/src/commands/destroy.ts new file mode 100644 index 000000000..9af53c621 --- /dev/null +++ b/packages/core/server/src/commands/destroy.ts @@ -0,0 +1,9 @@ +import Application from '../application'; + +export default (app: Application) => { + app.command('destroy').action(async (...cliArgs) => { + await app.destroy({ + cliArgs, + }); + }); +}; diff --git a/packages/core/server/src/commands/index.ts b/packages/core/server/src/commands/index.ts index e12c954fc..47d10428d 100644 --- a/packages/core/server/src/commands/index.ts +++ b/packages/core/server/src/commands/index.ts @@ -8,6 +8,9 @@ export function registerCli(app: Application) { require('./install').default(app); require('./migrator').default(app); require('./start').default(app); + require('./restart').default(app); + require('./stop').default(app); + require('./destroy').default(app); require('./upgrade').default(app); require('./pm').default(app); diff --git a/packages/core/server/src/commands/install.ts b/packages/core/server/src/commands/install.ts index a83a88bd1..9b672bcfa 100644 --- a/packages/core/server/src/commands/install.ts +++ b/packages/core/server/src/commands/install.ts @@ -1,4 +1,3 @@ -import chalk from 'chalk'; import Application from '../application'; export default (app: Application) => { @@ -6,40 +5,8 @@ export default (app: Application) => { .command('install') .option('-f, --force') .option('-c, --clean') - .option('-s, --silent') - .option('-r, --retry [retry]') - .option('-I, --ignore-installed') .action(async (...cliArgs) => { - let installed = false; const [opts] = cliArgs; - - if (opts.ignoreInstalled) { - if (await app.isInstalled()) { - console.log('Application installed'); - return; - } - } - - if (!opts?.clean && !opts?.force) { - if (await app.isInstalled()) { - installed = true; - if (!opts.silent) { - console.log('NocoBase is already installed. To reinstall, please execute:'); - console.log(); - const command = '$ yarn nocobase install -f'; - console.log(chalk.yellow(command)); - console.log(); - console.log(chalk.red('This operation will clear the database!!!')); - console.log(); - } - return; - } - } - - if (!opts.silent || !installed) { - console.log(`Start installing NocoBase`); - } - await app.install({ cliArgs, clean: opts.clean, @@ -47,9 +14,5 @@ export default (app: Application) => { force: opts.force, }, }); - - await app.stop({ - cliArgs, - }); }); }; diff --git a/packages/core/server/src/commands/pm.ts b/packages/core/server/src/commands/pm.ts index f1bddfd1c..9befc0112 100644 --- a/packages/core/server/src/commands/pm.ts +++ b/packages/core/server/src/commands/pm.ts @@ -1,18 +1,51 @@ import Application from '../application'; export default (app: Application) => { - app - .command('pm') - .argument('') - .arguments('') - .option('-S, --skip-yarn-install', 'skip yarn install') - .action(async (method, plugins, options, ...args) => { - if (method === 'add' && !options.skipYarnInstall) { - const { run } = require('@nocobase/cli/src/util'); - console.log('Install dependencies and rebuild workspaces'); - await run('yarn', ['install']); - } + const pm = app.command('pm'); - app.pm.clientWrite({ method, plugins }); + pm.command('create') + .arguments('plugin') + .action(async (plugin) => { + await app.pm.create(plugin); + }); + + pm.command('add') + .arguments('plugin') + .action(async (plugin) => { + await app.pm.add(plugin); + }); + + pm.command('enable') + .arguments('') + .action(async (plugins) => { + try { + await app.pm.enable(plugins); + } catch (error) { + app.log.debug(`Failed to enable plugin: ${error.message}`); + app.setMaintainingMessage(`Failed to enable plugin: ${error.message}`); + await new Promise((resolve) => { + setTimeout(() => resolve(null), 10000); + }); + } + }); + + pm.command('disable') + .arguments('') + .action(async (plugins) => { + try { + await app.pm.disable(plugins); + } catch (error) { + app.log.debug(`Failed to disable plugin: ${error.message}`); + app.setMaintainingMessage(`Failed to disable plugin: ${error.message}`); + await new Promise((resolve) => { + setTimeout(() => resolve(null), 10000); + }); + } + }); + + pm.command('remove') + .arguments('') + .action(async (plugins) => { + await app.pm.remove(plugins); }); }; diff --git a/packages/core/server/src/commands/restart.ts b/packages/core/server/src/commands/restart.ts new file mode 100644 index 000000000..f92d07735 --- /dev/null +++ b/packages/core/server/src/commands/restart.ts @@ -0,0 +1,9 @@ +import Application from '../application'; + +export default (app: Application) => { + app.command('restart').action(async (...cliArgs) => { + await app.restart({ + cliArgs, + }); + }); +}; diff --git a/packages/core/server/src/commands/start.ts b/packages/core/server/src/commands/start.ts index 605e2963d..35899bc08 100644 --- a/packages/core/server/src/commands/start.ts +++ b/packages/core/server/src/commands/start.ts @@ -3,26 +3,24 @@ import Application from '../application'; export default (app: Application) => { app .command('start') - .option('-s, --silent') - .option('-p, --port [post]') - .option('-h, --host [host]') .option('--db-sync') + .option('--quickstart') .action(async (...cliArgs) => { const [opts] = cliArgs; - const port = opts.port || process.env.APP_PORT || 13000; - const host = opts.host || process.env.APP_HOST || '0.0.0.0'; + + if (opts.quickstart) { + if (await app.isInstalled()) { + app.log.debug('installed....'); + await app.upgrade(); + } else { + await app.install(); + } + } await app.start({ dbSync: opts?.dbSync, cliArgs, - listen: { - port, - host, - }, + checkInstall: true, }); - - if (!opts.silent) { - console.log(`🚀 NocoBase server running at: http://${host === '0.0.0.0' ? 'localhost' : host}:${port}/`); - } }); }; diff --git a/packages/core/server/src/commands/stop.ts b/packages/core/server/src/commands/stop.ts new file mode 100644 index 000000000..58ae687af --- /dev/null +++ b/packages/core/server/src/commands/stop.ts @@ -0,0 +1,9 @@ +import Application from '../application'; + +export default (app: Application) => { + app.command('stop').action(async (...cliArgs) => { + await app.stop({ + cliArgs, + }); + }); +}; diff --git a/packages/core/server/src/commands/upgrade.ts b/packages/core/server/src/commands/upgrade.ts index 15bb7864b..056ff3b04 100644 --- a/packages/core/server/src/commands/upgrade.ts +++ b/packages/core/server/src/commands/upgrade.ts @@ -9,9 +9,6 @@ export default (app: Application) => { const [opts] = cliArgs; console.log('upgrading...'); await app.upgrade(); - await app.stop({ - cliArgs, - }); console.log(chalk.green(`✨ NocoBase has been upgraded to v${app.getVersion()}`)); }); }; diff --git a/packages/core/server/src/errors/application-not-install.ts b/packages/core/server/src/errors/application-not-install.ts new file mode 100644 index 000000000..ff2ad030c --- /dev/null +++ b/packages/core/server/src/errors/application-not-install.ts @@ -0,0 +1,9 @@ +export class ApplicationNotInstall extends Error { + code: string; + + constructor(message) { + super(message); + + this.code = 'APP_NOT_INSTALLED_ERROR'; + } +} diff --git a/packages/core/server/src/gateway/errors.ts b/packages/core/server/src/gateway/errors.ts new file mode 100644 index 000000000..7557cab0c --- /dev/null +++ b/packages/core/server/src/gateway/errors.ts @@ -0,0 +1,127 @@ +import { AppSupervisor } from '../app-supervisor'; +import lodash from 'lodash'; + +interface AppError { + status: number; + message: any; + command?: any; + maintaining: boolean; + code: any; +} + +interface AppErrors { + [key: string]: Omit & { + code?: any; + }; +} + +export const errors: AppErrors = { + APP_NOT_FOUND: { + status: 404, + message: ({ appName }) => `application ${appName} not found`, + maintaining: true, + }, + + APP_ERROR: { + status: 503, + message: ({ app }) => { + return AppSupervisor.getInstance().appErrors[app.name]?.message; + }, + code: ({ app }): string => { + const error = AppSupervisor.getInstance().appErrors[app.name]; + return error['code'] || 'APP_ERROR'; + }, + command: ({ app }) => app.getMaintaining().command, + maintaining: true, + }, + + APP_STARTING: { + status: 503, + message: ({ app }) => app.maintainingMessage, + maintaining: true, + }, + + APP_STOPPED: { + status: 503, + message: ({ app }) => `application ${app.name} is stopped`, + maintaining: true, + }, + + APP_INITIALIZED: { + status: 503, + message: ({ app }) => `application ${app.name} is initialized, waiting for command`, + maintaining: true, + }, + + APP_INITIALIZING: { + status: 503, + message: ({ appName }) => `application ${appName} is initializing`, + maintaining: true, + }, + + COMMAND_ERROR: { + status: 503, + maintaining: true, + message: ({ app }) => app.getMaintaining().error.message, + command: ({ app }) => app.getMaintaining().command, + }, + + COMMAND_END: { + status: 503, + maintaining: true, + message: ({ app }) => `${app.getMaintaining().command.name} running end`, + command: ({ app }) => app.getMaintaining().command, + }, + + APP_COMMANDING: { + status: 503, + maintaining: true, + message: ({ app, message }) => message || app.maintainingMessage, + command: ({ app, command }) => command || app.getMaintaining().command, + }, + + APP_RUNNING: { + status: 200, + maintaining: false, + message: ({ message, app }) => message || `application ${app.name} is running`, + }, + + UNKNOWN_ERROR: { + status: 500, + message: 'unknown error', + maintaining: true, + }, +}; + +export function getErrorWithCode(errorCode: string): AppError { + const rawCode = errorCode; + errorCode = lodash.snakeCase(errorCode).toUpperCase(); + + if (!errors[errorCode] && errors[`APP_${errorCode}`]) { + errorCode = `APP_${errorCode}`; + } + + if (!errors[errorCode]) { + errorCode = 'UNKNOWN_ERROR'; + } + + const error = lodash.cloneDeep(errors[errorCode]); + + if (!error.code) { + error['code'] = errorCode == 'UNKNOWN_ERROR' ? rawCode : errorCode; + } + + return error as AppError; +} + +export function applyErrorWithArgs(error: AppError, options) { + const functionKeys = Object.keys(error).filter((key) => typeof error[key] === 'function'); + const functionResults = functionKeys.map((key) => { + return error[key](options); + }); + + return { + ...error, + ...lodash.zipObject(functionKeys, functionResults), + }; +} diff --git a/packages/core/server/src/gateway/handle-plugin-static-file.ts b/packages/core/server/src/gateway/handle-plugin-static-file.ts new file mode 100644 index 000000000..36207ea53 --- /dev/null +++ b/packages/core/server/src/gateway/handle-plugin-static-file.ts @@ -0,0 +1,81 @@ +import { IncomingMessage, ServerResponse } from 'http'; +import path from 'path'; +import fs from 'fs'; + +const cwd = process.cwd(); +const NODE_MODULES = path.join(cwd, 'node_modules'); + +const PREFIX = '/api/plugins/client/'; + +const isMatchClientStaticUrl = (url: string) => { + return url.startsWith(PREFIX); +}; + +/** + * get package name from url + * + * @example + * /api/plugins/client/@nocobase/plugin-acl/index.js => @nocobase/plugin-acl + * /api/plugins/client/my-plugin/README.md => my-plugin + */ +const getPackageName = (url: string) => { + const urlArr = url.split('/'); + return urlArr[4].startsWith('@') ? `${urlArr[4]}/${urlArr[5]}` : urlArr[4]; +}; + +/** + * get plugin client static file real path + * + * @example + * /api/plugins/client/@nocobase/plugin-acl/index.js => /node_modules/@nocobase/plugin-acl/dist/client/index.js + * /api/plugins/client/my-plugin/README.md => /node_modules/my-plugin/dist/client/README.md + */ +const getRealPath = (packageName: string, url: string) => { + const ext = path.extname(url); + const filePath = url.replace(`${PREFIX}${packageName}/`, ''); + if (ext.toLowerCase() === '.md') { + return path.join(NODE_MODULES, packageName, filePath); + } else { + return path.join(NODE_MODULES, packageName, 'dist', 'client', filePath); + } +}; + +export async function handlePluginStaticFile(req: IncomingMessage, res: ServerResponse): Promise { + if (isMatchClientStaticUrl(req.url)) { + // TODO: check packageName in plugins + const packageName = getPackageName(req.url); + + const realPath = getRealPath(packageName, req.url); + + try { + // get file stats + const stats = await fs.promises.stat(realPath); + + const ifModifiedSince = req.headers['if-modified-since']; + + const lastModified = stats.mtime.toUTCString(); + + // check cache headers + if (ifModifiedSince === lastModified) { + res.statusCode = 304; + return true; + } + + const relativePath = path.relative(cwd, realPath); + + res.writeHead(200, { + 'Content-Length': stats.size, + }); + + const readStream = fs.createReadStream(relativePath); + readStream.pipe(res); + } catch (e) { + res.writeHead(404); + res.end(); + } + + return true; + } + + return false; +} diff --git a/packages/core/server/src/gateway/index.ts b/packages/core/server/src/gateway/index.ts new file mode 100644 index 000000000..a4a6006b3 --- /dev/null +++ b/packages/core/server/src/gateway/index.ts @@ -0,0 +1,310 @@ +import { Command } from 'commander'; +import compression from 'compression'; +import { EventEmitter } from 'events'; +import http, { IncomingMessage, ServerResponse } from 'http'; +import { promisify } from 'node:util'; +import { resolve } from 'path'; +import qs from 'qs'; +import handler from 'serve-handler'; +import { parse } from 'url'; +import { AppSupervisor } from '../app-supervisor'; +import { ApplicationOptions } from '../application'; +import { applyErrorWithArgs, getErrorWithCode } from './errors'; +import { IPCSocketClient } from './ipc-socket-client'; +import { IPCSocketServer } from './ipc-socket-server'; +import { WSServer } from './ws-server'; + +const compress = promisify(compression()); + +export interface IncomingRequest { + url: string; + headers: any; +} + +export type AppSelector = (req: IncomingRequest) => string | Promise; + +interface StartHttpServerOptions { + port: number; + host: string; + callback?: (server: http.Server) => void; +} + +interface RunOptions { + mainAppOptions: ApplicationOptions; +} + +export class Gateway extends EventEmitter { + private static instance: Gateway; + /** + * use main app as default app to handle request + */ + appSelector: AppSelector; + public server: http.Server | null = null; + public ipcSocketServer: IPCSocketServer | null = null; + private port: number = process.env.APP_PORT ? parseInt(process.env.APP_PORT) : null; + private host = '0.0.0.0'; + private wsServer: WSServer; + private socketPath = resolve(process.cwd(), 'storage', 'gateway.sock'); + + private constructor() { + super(); + this.reset(); + } + + public static getInstance(options: any = {}): Gateway { + if (!Gateway.instance) { + Gateway.instance = new Gateway(); + } + + return Gateway.instance; + } + + destroy() { + this.reset(); + Gateway.instance = null; + } + + public reset() { + this.setAppSelector(async (req) => { + const appName = qs.parse(parse(req.url).query)?.__appName; + if (appName) { + return appName; + } + + if (req.headers['x-app']) { + return req.headers['x-app']; + } + + return null; + }); + + if (this.server) { + this.server.close(); + this.server = null; + } + + if (this.ipcSocketServer) { + this.ipcSocketServer.close(); + this.ipcSocketServer = null; + } + } + + setAppSelector(selector: AppSelector) { + this.appSelector = selector; + this.emit('appSelectorChanged'); + } + + responseError( + res: ServerResponse, + error: { + status: number; + maintaining: boolean; + message: string; + code: string; + }, + ) { + res.setHeader('Content-Type', 'application/json'); + res.statusCode = error.status; + res.end(JSON.stringify({ error })); + } + + responseErrorWithCode(code, res, options) { + this.responseError(res, applyErrorWithArgs(getErrorWithCode(code), options)); + } + + async requestHandler(req: IncomingMessage, res: ServerResponse) { + const { pathname } = parse(req.url); + + if (pathname.startsWith('/storage/uploads/')) { + await compress(req, res); + return handler(req, res, { + public: resolve(process.cwd()), + }); + } + + if (pathname.startsWith('/api/plugins/client/')) { + await compress(req, res); + return handler(req, res, { + public: resolve(process.cwd(), 'node_modules'), + rewrites: [ + { + source: '/api/plugins/client/:plugin/index.js', + destination: '/:plugin/dist/client/index.js', + }, + { + source: '/api/plugins/client/@:org/:plugin/index.js', + destination: '/@:org/:plugin/dist/client/index.js', + }, + ], + }); + } + + if (!pathname.startsWith('/api')) { + await compress(req, res); + return handler(req, res, { + public: `${process.env.APP_PACKAGE_ROOT}/dist/client`, + rewrites: [{ source: '/**', destination: '/index.html' }], + }); + } + + const handleApp = await this.getRequestHandleAppName(req as IncomingRequest); + + const hasApp = AppSupervisor.getInstance().hasApp(handleApp); + + if (!hasApp) { + AppSupervisor.getInstance().bootStrapApp(handleApp); + } + + const appStatus = AppSupervisor.getInstance().getAppStatus(handleApp, 'initializing'); + + if (appStatus === 'not_found') { + this.responseErrorWithCode('APP_NOT_FOUND', res, { appName: handleApp }); + return; + } + + if (appStatus === 'initializing') { + this.responseErrorWithCode('APP_INITIALIZING', res, { appName: handleApp }); + return; + } + + const app = await AppSupervisor.getInstance().getApp(handleApp); + + if (appStatus !== 'running') { + this.responseErrorWithCode(`${appStatus}`, res, { app }); + return; + } + + if (req.url.endsWith('/__health_check')) { + res.statusCode = 200; + res.end('ok'); + return; + } + + app.callback()(req, res); + } + + async getRequestHandleAppName(req: IncomingRequest) { + return (await this.appSelector(req)) || 'main'; + } + + getCallback() { + return this.requestHandler.bind(this); + } + + async run(options: RunOptions) { + const isStart = this.isStart(); + if (isStart) { + const startOptions = this.getStartOptions(); + const port = startOptions.port || process.env.APP_PORT || 13000; + const host = startOptions.host || process.env.APP_HOST || '0.0.0.0'; + + this.start({ + port, + host, + }); + } else if (!this.isHelp()) { + const ipcClient = await this.tryConnectToIPCServer(); + + if (ipcClient) { + ipcClient.write({ type: 'passCliArgv', payload: { argv: process.argv } }); + ipcClient.close(); + return; + } + } + + const mainApp = AppSupervisor.getInstance().bootMainApp(options.mainAppOptions); + mainApp.runAsCLI(); + } + + isStart() { + const argv = process.argv; + return argv[2] === 'start'; + } + + isHelp() { + const argv = process.argv; + return argv[2] === 'help'; + } + + getStartOptions() { + const argv = process.argv; + const program = new Command(); + + program + .allowUnknownOption() + .option('-s, --silent') + .option('-p, --port [post]') + .option('-h, --host [host]') + .option('--db-sync') + .parse(process.argv); + const options = program.opts(); + + return options; + } + + start(options: StartHttpServerOptions) { + this.startHttpServer(options); + this.startIPCSocketServer(); + } + + startIPCSocketServer() { + this.ipcSocketServer = IPCSocketServer.buildServer(this.socketPath); + } + + startHttpServer(options: StartHttpServerOptions) { + if (options?.port !== null) { + this.port = options.port; + } + + if (options?.host) { + this.host = options.host; + } + + if (this.port === null) { + console.log('gateway port is not set, http server will not start'); + return; + } + + this.server = http.createServer(this.getCallback()); + + this.wsServer = new WSServer(); + + this.server.on('upgrade', (request, socket, head) => { + const { pathname } = parse(request.url); + + if (pathname === '/ws') { + this.wsServer.wss.handleUpgrade(request, socket, head, (ws) => { + this.wsServer.wss.emit('connection', ws, request); + }); + } else { + socket.destroy(); + } + }); + + this.server.listen(this.port, this.host, () => { + console.log(`Gateway HTTP Server running at http://${this.host}:${this.port}/`); + if (options?.callback) { + options.callback(this.server); + } + }); + } + + async tryConnectToIPCServer() { + try { + const ipcClient = await this.getIPCSocketClient(); + return ipcClient; + } catch (e) { + // console.log(e); + return false; + } + } + + async getIPCSocketClient() { + return await IPCSocketClient.getConnection(this.socketPath); + } + + close() { + this.server?.close(); + this.wsServer?.close(); + } +} diff --git a/packages/core/server/src/gateway/ipc-socket-client.ts b/packages/core/server/src/gateway/ipc-socket-client.ts new file mode 100644 index 000000000..c3e79f6a7 --- /dev/null +++ b/packages/core/server/src/gateway/ipc-socket-client.ts @@ -0,0 +1,34 @@ +import net from 'net'; + +const writeJSON = (socket: net.Socket, data: object) => { + socket.write(JSON.stringify(data) + '\n', 'utf8'); +}; + +export class IPCSocketClient { + client: net.Socket; + + constructor(client: net.Socket) { + this.client = client; + } + + static async getConnection(serverPath: string) { + return new Promise((resolve, reject) => { + const client = net.createConnection({ path: serverPath }, () => { + // 'connect' listener. + console.log('connected to server!'); + resolve(new IPCSocketClient(client)); + }); + client.on('error', (err) => { + reject(err); + }); + }); + } + + close() { + this.client.end(); + } + + write(data: any) { + writeJSON(this.client, data); + } +} diff --git a/packages/core/server/src/gateway/ipc-socket-server.ts b/packages/core/server/src/gateway/ipc-socket-server.ts new file mode 100644 index 000000000..63fc753c9 --- /dev/null +++ b/packages/core/server/src/gateway/ipc-socket-server.ts @@ -0,0 +1,63 @@ +import net from 'net'; +import fs from 'fs'; +import { Gateway } from '../gateway'; +import { AppSupervisor } from '../app-supervisor'; + +export class IPCSocketServer { + socketServer: net.Server; + + constructor(server: net.Server) { + this.socketServer = server; + } + + static buildServer(socketPath: string) { + // try to unlink the socket from a previous run + if (fs.existsSync(socketPath)) { + fs.unlinkSync(socketPath); + } + + const socketServer = net.createServer((c) => { + console.log('client connected'); + + c.on('end', () => { + console.log('client disconnected'); + }); + + c.on('data', (data) => { + const dataAsString = data.toString(); + const messages = dataAsString.split('\n'); + + for (const message of messages) { + if (message.length === 0) { + continue; + } + + const dataObj = JSON.parse(message); + + IPCSocketServer.handleClientMessage(dataObj); + } + }); + }); + + socketServer.listen(socketPath, () => { + console.log(`Gateway IPC Server running at ${socketPath}`); + }); + + return new IPCSocketServer(socketServer); + } + + static async handleClientMessage({ type, payload }) { + console.log(`cli received message ${type}`); + + if (type === 'passCliArgv') { + const argv = payload.argv; + + const mainApp = await AppSupervisor.getInstance().getApp('main'); + mainApp.runAsCLI(argv); + } + } + + close() { + this.socketServer.close(); + } +} diff --git a/packages/core/server/src/gateway/ws-server.ts b/packages/core/server/src/gateway/ws-server.ts new file mode 100644 index 000000000..3e3a77b53 --- /dev/null +++ b/packages/core/server/src/gateway/ws-server.ts @@ -0,0 +1,181 @@ +import { Gateway, IncomingRequest } from '../gateway'; +import WebSocket from 'ws'; +import { nanoid } from 'nanoid'; +import { IncomingMessage } from 'http'; +import { AppSupervisor } from '../app-supervisor'; +import { applyErrorWithArgs, getErrorWithCode } from './errors'; +import lodash from 'lodash'; + +declare class WebSocketWithId extends WebSocket { + id: string; +} + +interface WebSocketClient { + ws: WebSocketWithId; + tags: string[]; + url: string; + headers: any; + app?: string; +} + +function getPayloadByErrorCode(code, options) { + const error = getErrorWithCode(code); + return lodash.omit(applyErrorWithArgs(error, options), ['status', 'maintaining']); +} + +export class WSServer { + wss: WebSocket.Server; + webSocketClients = new Map(); + + constructor() { + this.wss = new WebSocket.Server({ noServer: true }); + + this.wss.on('connection', (ws: WebSocketWithId, request: IncomingMessage) => { + const client = this.addNewConnection(ws, request); + + console.log(`new client connected ${ws.id}`); + + ws.on('error', () => { + this.removeConnection(ws.id); + }); + + ws.on('close', () => { + this.removeConnection(ws.id); + }); + }); + + Gateway.getInstance().on('appSelectorChanged', () => { + // reset connection app tags + this.loopThroughConnections(async (client) => { + const handleAppName = await Gateway.getInstance().getRequestHandleAppName({ + url: client.url, + headers: client.headers, + }); + + client.tags = client.tags.filter((tag) => !tag.startsWith('app#')); + client.tags.push(`app#${handleAppName}`); + + AppSupervisor.getInstance().bootStrapApp(handleAppName); + }); + }); + + AppSupervisor.getInstance().on('appMaintainingMessageChanged', async ({ appName, message, command, status }) => { + const app = await AppSupervisor.getInstance().getApp(appName, { + withOutBootStrap: true, + }); + + const payload = getPayloadByErrorCode(status, { + app, + message, + command, + }); + + this.sendToConnectionsByTag('app', appName, { + type: 'maintaining', + payload, + }); + }); + + AppSupervisor.getInstance().on('appStatusChanged', async ({ appName, status }) => { + const app = await AppSupervisor.getInstance().getApp(appName, { + withOutBootStrap: true, + }); + + const payload = getPayloadByErrorCode(status, { app, appName }); + console.log(`send payload ${JSON.stringify(payload)}`); + this.sendToConnectionsByTag('app', appName, { + type: 'maintaining', + payload, + }); + }); + } + + addNewConnection(ws: WebSocketWithId, request: IncomingMessage) { + const id = nanoid(); + + ws.id = id; + + this.webSocketClients.set(id, { + ws, + tags: [], + url: request.url, + headers: request.headers, + }); + + this.setClientApp(this.webSocketClients.get(id)); + + return this.webSocketClients.get(id); + } + + async setClientApp(client: WebSocketClient) { + const req: IncomingRequest = { + url: client.url, + headers: client.headers, + }; + + const handleAppName = await Gateway.getInstance().getRequestHandleAppName(req); + + client.app = handleAppName; + console.log(`client tags: app#${handleAppName}`); + client.tags.push(`app#${handleAppName}`); + + const hasApp = AppSupervisor.getInstance().hasApp(handleAppName); + + if (!hasApp) { + AppSupervisor.getInstance().bootStrapApp(handleAppName); + } + + const appStatus = AppSupervisor.getInstance().getAppStatus(handleAppName, 'initializing'); + + if (appStatus === 'not_found') { + this.sendMessageToConnection(client, { + type: 'maintaining', + payload: getPayloadByErrorCode('APP_NOT_FOUND', { appName: handleAppName }), + }); + return; + } + + if (appStatus === 'initializing') { + this.sendMessageToConnection(client, { + type: 'maintaining', + payload: getPayloadByErrorCode('APP_INITIALIZING', { appName: handleAppName }), + }); + + return; + } + + const app = await AppSupervisor.getInstance().getApp(handleAppName); + + this.sendMessageToConnection(client, { + type: 'maintaining', + payload: getPayloadByErrorCode(appStatus, { app }), + }); + } + + removeConnection(id: string) { + console.log(`client disconnected ${id}`); + this.webSocketClients.delete(id); + } + + sendMessageToConnection(client: WebSocketClient, sendMessage: object) { + client.ws.send(JSON.stringify(sendMessage)); + } + + sendToConnectionsByTag(tagName: string, tagValue: string, sendMessage: object) { + this.loopThroughConnections((client: WebSocketClient) => { + if (client.tags.includes(`${tagName}#${tagValue}`)) { + this.sendMessageToConnection(client, sendMessage); + } + }); + } + + loopThroughConnections(callback: (client: WebSocketClient) => void) { + this.webSocketClients.forEach((client) => { + callback(client); + }); + } + + close() { + this.wss.close(); + } +} diff --git a/packages/core/server/src/helper.ts b/packages/core/server/src/helper.ts index 29f55a709..9cca21603 100644 --- a/packages/core/server/src/helper.ts +++ b/packages/core/server/src/helper.ts @@ -1,6 +1,7 @@ import cors from '@koa/cors'; import Database from '@nocobase/database'; import Resourcer from '@nocobase/resourcer'; +import { Command } from 'commander'; import i18next from 'i18next'; import bodyParser from 'koa-bodyparser'; import Application, { ApplicationOptions } from './application'; @@ -78,3 +79,31 @@ export function registerMiddlewares(app: Application, options: ApplicationOption app.use(db2resource, { tag: 'db2resource', after: 'dataWrapping' }); app.use(app.resourcer.restApiMiddleware(), { tag: 'restApi', after: 'db2resource' }); } + +export const createAppProxy = (app: Application) => { + return new Proxy(app, { + get(target, prop, ...args) { + if (typeof prop === 'string' && ['on', 'once', 'addListener'].includes(prop)) { + return (eventName: string, listener: any) => { + listener['_reinitializable'] = true; + return target[prop](eventName, listener); + }; + } + return Reflect.get(target, prop, ...args); + }, + }); +}; + +export const getCommandFullName = (command: Command) => { + const names = []; + names.push(command.name()); + let parent = command?.parent; + while (parent) { + if (!parent?.parent) { + break; + } + names.unshift(parent.name()); + parent = parent.parent; + } + return names.join('.'); +}; diff --git a/packages/core/server/src/helpers/application-version.ts b/packages/core/server/src/helpers/application-version.ts new file mode 100644 index 000000000..ba9ced9dd --- /dev/null +++ b/packages/core/server/src/helpers/application-version.ts @@ -0,0 +1,56 @@ +import { Collection } from '@nocobase/database'; +import semver from 'semver'; +import Application from '../application'; + +export class ApplicationVersion { + protected app: Application; + protected collection: Collection; + + constructor(app: Application) { + this.app = app; + if (!app.db.hasCollection('applicationVersion')) { + app.db.collection({ + name: 'applicationVersion', + namespace: 'core.applicationVersion', + duplicator: 'required', + timestamps: false, + fields: [{ name: 'value', type: 'string' }], + }); + } + this.collection = this.app.db.getCollection('applicationVersion'); + } + + async get() { + if (await this.app.db.collectionExistsInDb('applicationVersion')) { + const model = await this.collection.model.findOne(); + if (!model) { + return null; + } + return model.get('value') as any; + } + return null; + } + + async update() { + await this.collection.sync(); + await this.collection.model.destroy({ + truncate: true, + }); + + await this.collection.model.create({ + value: this.app.getVersion(), + }); + } + + async satisfies(range: string) { + if (await this.app.db.collectionExistsInDb('applicationVersion')) { + const model: any = await this.collection.model.findOne(); + const version = model?.value as any; + if (!version) { + return true; + } + return semver.satisfies(version, range, { includePrerelease: true }); + } + return true; + } +} diff --git a/packages/core/server/src/index.ts b/packages/core/server/src/index.ts index a4181070d..470d31a31 100644 --- a/packages/core/server/src/index.ts +++ b/packages/core/server/src/index.ts @@ -1,4 +1,3 @@ -export { AppManager } from './app-manager'; export * from './application'; export { Application as default } from './application'; export * as middlewares from './middlewares'; @@ -6,3 +5,5 @@ export * from './migration'; export * from './plugin'; export * from './plugin-manager'; export * from './read-config'; +export * from './gateway'; +export * from './app-supervisor'; diff --git a/packages/core/server/src/locale/locale.ts b/packages/core/server/src/locale/locale.ts index 10a5d8b33..d51aab425 100644 --- a/packages/core/server/src/locale/locale.ts +++ b/packages/core/server/src/locale/locale.ts @@ -14,11 +14,17 @@ export class Locale { this.app = app; this.cache = createCache(); - this.app.on('afterLoad', () => this.load()); + this.app.on('afterLoad', async () => { + this.app.log.debug('load locale resource'); + this.app.setMaintainingMessage('load locale resource'); + await this.load(); + this.app.log.debug('locale resource loaded'); + this.app.setMaintainingMessage('locale resource loaded'); + }); } - load() { - this.getCacheResources(this.defaultLang); + async load() { + await this.get(this.defaultLang); } setLocaleFn(name: string, fn: (lang: string) => Promise) { @@ -30,6 +36,7 @@ export class Locale { resources: await this.getCacheResources(lang), }; for (const [name, fn] of this.localeFn) { + // this.app.log.debug(`load [${name}] locale resource `); const result = await this.wrapCache(`locale:${name}:${lang}`, async () => await fn(lang)); if (result) { defaults[name] = result; @@ -57,10 +64,12 @@ export class Locale { getResources(lang: string) { const resources = {}; - const plugins = this.app.pm.getPlugins(); - for (const name of plugins.keys()) { + const names = this.app.pm.getAliases(); + for (const name of names) { try { const packageName = PluginManager.getPackageName(name); + // this.app.log.debug(`load [${packageName}] locale resource `); + // this.app.setMaintainingMessage(`load [${packageName}] locale resource `); const res = getResource(packageName, lang); if (res) { resources[name] = { ...res }; @@ -69,10 +78,6 @@ export class Locale { // empty } } - const res = getResource('@nocobase/client', lang, false); - if (res) { - resources['client'] = { ...(resources['client'] || {}), ...res }; - } return resources; } } diff --git a/packages/core/server/src/plugin-manager/clientStaticMiddleware.ts b/packages/core/server/src/plugin-manager/clientStaticMiddleware.ts index 80dff1acb..b223f819c 100644 --- a/packages/core/server/src/plugin-manager/clientStaticMiddleware.ts +++ b/packages/core/server/src/plugin-manager/clientStaticMiddleware.ts @@ -1,10 +1,4 @@ -import fs from 'fs'; -import send from 'koa-send'; -import path from 'path'; - const PREFIX = '/api/plugins/client/'; -const cwd = process.cwd(); -const NODE_MODULES = path.join(cwd, 'node_modules'); /** * get plugin client static file url @@ -16,72 +10,3 @@ const NODE_MODULES = path.join(cwd, 'node_modules'); export const getPackageClientStaticUrl = (packageName: string, filePath: string) => { return `${PREFIX}${packageName}/${filePath}`; }; - -const isMatchClientStaticUrl = (url: string) => { - return url.startsWith(PREFIX); -}; - -/** - * get package name from url - * - * @example - * /api/plugins/client/@nocobase/plugin-acl/index.js => @nocobase/plugin-acl - * /api/plugins/client/my-plugin/README.md => my-plugin - */ -const getPackageName = (url: string) => { - const urlArr = url.split('/'); - return urlArr[4].startsWith('@') ? `${urlArr[4]}/${urlArr[5]}` : urlArr[4]; -}; - -/** - * get plugin client static file real path - * - * @example - * /api/plugins/client/@nocobase/plugin-acl/index.js => /node_modules/@nocobase/plugin-acl/dist/client/index.js - * /api/plugins/client/my-plugin/README.md => /node_modules/my-plugin/dist/client/README.md - */ -const getRealPath = (packageName: string, url: string) => { - const ext = path.extname(url); - const filePath = url.replace(`${PREFIX}${packageName}/`, ''); - if (ext.toLowerCase() === '.md') { - return path.join(NODE_MODULES, packageName, filePath); - } else { - return path.join(NODE_MODULES, packageName, 'dist', 'client', filePath); - } -}; - -/** - * send plugin client static file to browser. - * - * such as: - * /api/plugins/client/@nocobase/plugin-xxx/index.js - * /api/plugins/client/xxx/README.md - */ -export const clientStaticMiddleware = async (ctx, next) => { - if (isMatchClientStaticUrl(ctx.path)) { - // TODO: check packageName in plugins - const packageName = getPackageName(ctx.path); - - const realPath = getRealPath(packageName, ctx.path); - - // get file stats - const stats = await fs.promises.stat(realPath); - const ifModifiedSince = ctx.get('If-Modified-Since'); - const lastModified = stats.mtime.toUTCString(); - - // check cache headers - if (ifModifiedSince === lastModified) { - ctx.status = 304; - return; - } - - // `send` only accept relative path - const relativePath = path.relative(cwd, realPath); - await send(ctx, relativePath, { - setHeaders: (res) => { - res.setHeader('Last-Modified', lastModified); - }, - }); - } - await next(); -}; diff --git a/packages/core/server/src/plugin-manager/options/resource.ts b/packages/core/server/src/plugin-manager/options/resource.ts index 6f1295715..488069357 100644 --- a/packages/core/server/src/plugin-manager/options/resource.ts +++ b/packages/core/server/src/plugin-manager/options/resource.ts @@ -1,3 +1,5 @@ +import Application from '../../application'; + export default { name: 'pm', actions: { @@ -14,10 +16,11 @@ export default { async enable(ctx, next) { const pm = ctx.app.pm; const { filterByTk } = ctx.action.params; + const app = ctx.app as Application; if (!filterByTk) { ctx.throw(400, 'plugin name invalid'); } - await pm.enable(filterByTk); + app.runAsCLI(['pm', 'enable', filterByTk], { from: 'user' }); ctx.body = filterByTk; await next(); }, @@ -27,7 +30,8 @@ export default { if (!filterByTk) { ctx.throw(400, 'plugin name invalid'); } - await pm.disable(filterByTk); + const app = ctx.app as Application; + app.runAsCLI(['pm', 'disable', filterByTk], { from: 'user' }); ctx.body = filterByTk; await next(); }, @@ -41,7 +45,8 @@ export default { if (!filterByTk) { ctx.throw(400, 'plugin name invalid'); } - await pm.remove(filterByTk); + const app = ctx.app as Application; + app.runAsCLI(['pm', 'remove', filterByTk], { from: 'user' }); ctx.body = filterByTk; await next(); }, diff --git a/packages/core/server/src/plugin-manager/plugin-manager-repository.ts b/packages/core/server/src/plugin-manager/plugin-manager-repository.ts index c0246c0e0..20a7a2fa2 100644 --- a/packages/core/server/src/plugin-manager/plugin-manager-repository.ts +++ b/packages/core/server/src/plugin-manager/plugin-manager-repository.ts @@ -1,4 +1,5 @@ import { Repository } from '@nocobase/database'; +import lodash from 'lodash'; import { PluginManager } from './plugin-manager'; export class PluginManagerRepository extends Repository { @@ -17,13 +18,13 @@ export class PluginManagerRepository extends Repository { } async enable(name: string | string[]) { - const pluginNames = typeof name === 'string' ? [name] : name; - const plugins = pluginNames.map((name) => this.pm.plugins.get(name)); + const pluginNames = lodash.castArray(name); + const plugins = pluginNames.map((name) => this.pm.get(name)); for (const plugin of plugins) { const requiredPlugins = plugin.requiredPlugins(); for (const requiredPluginName of requiredPlugins) { - const requiredPlugin = this.pm.plugins.get(requiredPluginName); + const requiredPlugin = this.pm.get(requiredPluginName); if (!requiredPlugin.enabled) { throw new Error(`${plugin.name} plugin need ${requiredPluginName} plugin enabled`); } @@ -47,11 +48,17 @@ export class PluginManagerRepository extends Repository { } async disable(name: string | string[]) { - const pluginNames = typeof name === 'string' ? [name] : name; + name = lodash.cloneDeep(name); + + const pluginNames = lodash.castArray(name); + console.log(`disable ${name}, ${pluginNames}`); + const filter = { + name, + }; + + console.log(JSON.stringify(filter, null, 2)); await this.update({ - filter: { - name, - }, + filter, values: { enabled: false, installed: false, @@ -60,19 +67,38 @@ export class PluginManagerRepository extends Repository { return pluginNames; } - async load() { - // sort plugins by id - const items = await this.find({ - sort: 'id', - }); + async getItems() { + try { + // sort plugins by id + return await this.find({ + sort: 'id', + }); + } catch (error) { + await this.collection.sync({ + alter: { + drop: false, + }, + force: false, + }); + return await this.find({ + sort: 'id', + }); + } + } + + async init() { + const exists = await this.collection.existsInDb(); + if (!exists) { + return; + } + + const items = await this.getItems(); for (const item of items) { - await this.pm.addStatic(item.get('name'), { - ...item.get('options'), - name: item.get('name'), - version: item.get('version'), - enabled: item.get('enabled'), - async: true, + const { options, ...others } = item.toJSON(); + await this.pm.add(item.get('name'), { + ...others, + ...options, }); } } diff --git a/packages/core/server/src/plugin-manager/plugin-manager.ts b/packages/core/server/src/plugin-manager/plugin-manager.ts index acb75ab52..83f955686 100644 --- a/packages/core/server/src/plugin-manager/plugin-manager.ts +++ b/packages/core/server/src/plugin-manager/plugin-manager.ts @@ -1,13 +1,12 @@ import { CleanOptions, Collection, SyncOptions } from '@nocobase/database'; import { requireModule } from '@nocobase/utils'; import execa from 'execa'; -import fs from 'fs'; +import _ from 'lodash'; import net from 'net'; import { resolve } from 'path'; -import xpipe from 'xpipe'; import Application from '../application'; +import { createAppProxy } from '../helper'; import { Plugin } from '../plugin'; -import { clientStaticMiddleware } from './clientStaticMiddleware'; import collectionOptions from './options/collection'; import resourceOptions from './options/resource'; import { PluginManagerRepository } from './plugin-manager-repository'; @@ -23,31 +22,28 @@ export interface InstallOptions { sync?: SyncOptions; } +export class AddPresetError extends Error {} + export class PluginManager { app: Application; collection: Collection; - repository: PluginManagerRepository; - plugins = new Map(); + _repository: PluginManagerRepository; + pluginInstances = new Map(); + pluginAliases = new Map(); server: net.Server; - pmSock: string; - _tmpPluginArgs = []; - constructor(options: PluginManagerOptions) { + constructor(public options: PluginManagerOptions) { this.app = options.app; - const f = resolve(process.cwd(), 'storage', 'pm.sock'); - this.pmSock = xpipe.eq(this.app.options.pmSock || f); this.app.db.registerRepositories({ PluginManagerRepository, }); this.collection = this.app.db.collection(collectionOptions); - this.repository = this.collection.repository as PluginManagerRepository; - this.repository.setPluginManager(this); + this._repository = this.collection.repository as PluginManagerRepository; + this._repository.setPluginManager(this); this.app.resourcer.define(resourceOptions); - this.app.use(clientStaticMiddleware); - this.app.resourcer.use(async (ctx, next) => { await next(); const { resourceName, actionName } = ctx.action; @@ -72,297 +68,10 @@ export class PluginManager { name: 'pm', actions: ['pm:*', 'applicationPlugins:list'], }); - - this.app.on('beforeLoad', async (app, options) => { - if (options?.method && ['install', 'upgrade'].includes(options.method)) { - await this.collection.sync(); - } - - const exists = await this.app.db.collectionExistsInDb('applicationPlugins'); - - if (!exists) { - this.app.log.warn(`applicationPlugins collection not exists in ${this.app.name}`); - return; - } - - if (options?.method !== 'install' || options.reload) { - await this.repository.load(); - } - }); - - this.app.on('beforeUpgrade', async () => { - await this.collection.sync(); - }); - - this.addStaticMultiple(options.plugins); } - addStaticMultiple(plugins: any) { - for (const plugin of plugins || []) { - if (typeof plugin == 'string') { - this.addStatic(plugin); - } else { - this.addStatic(...plugin); - } - } - } - - getPlugins() { - return this.plugins; - } - - get(name: string) { - return this.plugins.get(name); - } - - has(name: string) { - return this.plugins.has(name); - } - - clientWrite(data: any) { - const { method, plugins } = data; - if (method === 'create') { - try { - console.log(method, plugins); - this[method](plugins); - } catch (error) { - console.error(error.message); - } - return; - } - const client = new net.Socket(); - client.connect(this.pmSock, () => { - client.write(JSON.stringify(data)); - client.end(); - }); - client.on('error', async () => { - try { - console.log(method, plugins); - await this[method](plugins); - } catch (error) { - console.error(error.message); - } - }); - } - - async listen(): Promise { - this.server = net.createServer((socket) => { - socket.on('data', async (data) => { - const { method, plugins } = JSON.parse(data.toString()); - try { - console.log(method, plugins); - await this[method](plugins); - } catch (error) { - console.error(error.message); - } - }); - socket.pipe(socket); - }); - - if (fs.existsSync(this.pmSock)) { - await fs.promises.unlink(this.pmSock); - } - return new Promise((resolve) => { - this.server.listen(this.pmSock, () => { - resolve(this.server); - }); - }); - } - - async create(name: string | string[]) { - console.log('creating...'); - const pluginNames = Array.isArray(name) ? name : [name]; - const { run } = require('@nocobase/cli/src/util'); - const createPlugin = async (name) => { - const { PluginGenerator } = require('@nocobase/cli/src/plugin-generator'); - const generator = new PluginGenerator({ - cwd: resolve(process.cwd(), name), - args: {}, - context: { - name, - }, - }); - await generator.run(); - }; - await Promise.all(pluginNames.map((pluginName) => createPlugin(pluginName))); - await run('yarn', ['install']); - } - - clone() { - const pm = new PluginManager({ - app: this.app, - }); - for (const arg of this._tmpPluginArgs) { - pm.addStatic(...arg); - } - return pm; - } - - addStatic(plugin?: any, options?: any) { - if (!options?.async) { - this._tmpPluginArgs.push([plugin, options]); - } - - let name: string; - if (typeof plugin === 'string') { - name = plugin; - plugin = PluginManager.resolvePlugin(plugin); - } else { - name = plugin.name; - if (!name) { - throw new Error(`plugin name invalid`); - } - } - - const instance = new plugin(this.app, { - name, - enabled: true, - ...options, - }); - - const pluginName = instance.getName(); - - if (this.plugins.has(pluginName)) { - throw new Error(`plugin name [${pluginName}] already exists`); - } - - this.plugins.set(pluginName, instance); - return instance; - } - - async add(plugin: any, options: any = {}, transaction?: any) { - if (Array.isArray(plugin)) { - const t = transaction || (await this.app.db.sequelize.transaction()); - try { - const items = []; - - for (const p of plugin) { - items.push(await this.add(p, options, t)); - } - - await t.commit(); - return items; - } catch (error) { - await t.rollback(); - throw error; - } - } - - const packageName = await PluginManager.findPackage(plugin); - - const instance = this.addStatic(plugin, { - ...options, - async: true, - }); - - const model = await this.repository.findOne({ - transaction, - filter: { name: plugin }, - }); - - const packageJson = PluginManager.getPackageJson(packageName); - - if (!model) { - const { enabled, builtIn, installed, ...others } = options; - await this.repository.create({ - transaction, - values: { - name: plugin, - version: packageJson.version, - enabled: !!enabled, - builtIn: !!builtIn, - installed: !!installed, - options: { - ...others, - }, - }, - }); - } - return instance; - } - - async load(options: any = {}) { - for (const [name, plugin] of this.plugins) { - if (!plugin.enabled) { - continue; - } - await plugin.beforeLoad(); - } - - for (const [name, plugin] of this.plugins) { - if (!plugin.enabled) { - continue; - } - await this.app.emitAsync('beforeLoadPlugin', plugin, options); - await plugin.load(); - await this.app.emitAsync('afterLoadPlugin', plugin, options); - } - } - - async install(options: InstallOptions = {}) { - for (const [name, plugin] of this.plugins) { - if (!plugin.enabled) { - continue; - } - await this.app.emitAsync('beforeInstallPlugin', plugin, options); - await plugin.install(options); - await this.app.emitAsync('afterInstallPlugin', plugin, options); - } - } - - async enable(name: string | string[]) { - try { - const pluginNames = await this.repository.enable(name); - await this.app.reload(); - - await this.app.db.sync(); - for (const pluginName of pluginNames) { - const plugin = this.app.getPlugin(pluginName); - if (!plugin) { - throw new Error(`${name} plugin does not exist`); - } - if (!plugin.options.installed) { - await plugin.install(); - plugin.options.installed = true; - } - await plugin.afterEnable(); - } - - await this.app.emitAsync('afterEnablePlugin', name); - } catch (error) { - throw error; - } - } - - async disable(name: string | string[]) { - try { - const pluginNames = await this.repository.disable(name); - await this.app.reload(); - for (const pluginName of pluginNames) { - const plugin = this.app.getPlugin(pluginName); - if (!plugin) { - throw new Error(`${name} plugin does not exist`); - } - await plugin.afterDisable(); - } - - await this.app.emitAsync('afterDisablePlugin', name); - } catch (error) { - throw error; - } - } - - async remove(name: string | string[]) { - const pluginNames = typeof name === 'string' ? [name] : name; - for (const pluginName of pluginNames) { - const plugin = this.app.getPlugin(pluginName); - if (!plugin) { - throw new Error(`${name} plugin does not exist`); - } - await plugin.remove(); - } - await this.repository.remove(name); - this.app.reload(); + get repository() { + return this.app.db.getRepository('applicationPlugins') as PluginManagerRepository; } static getPackageJson(packageName: string) { @@ -412,9 +121,354 @@ export class PluginManager { throw new Error(`No available packages found, ${name} plugin does not exist`); } - static resolvePlugin(pluginName: string) { - const packageName = this.getPackageName(pluginName); - return requireModule(packageName); + static resolvePlugin(pluginName: string | typeof Plugin) { + if (typeof pluginName === 'string') { + const packageName = this.getPackageName(pluginName); + return requireModule(packageName); + } else { + return pluginName; + } + } + + addPreset(plugin: string | typeof Plugin, options: any = {}) { + if (this.app.loaded) { + throw new AddPresetError('must be added before executing app.load()'); + } + if (!this.options.plugins) { + this.options.plugins = []; + } + this.options.plugins.push([plugin, options]); + } + + getPlugins() { + return this.pluginInstances; + } + + getAliases() { + return this.pluginAliases.keys(); + } + + get(name: string | typeof Plugin) { + if (typeof name === 'string') { + return this.pluginAliases.get(name); + } + return this.pluginInstances.get(name); + } + + has(name: string | typeof Plugin) { + if (typeof name === 'string') { + return this.pluginAliases.has(name); + } + return this.pluginInstances.has(name); + } + + del(name: string | typeof Plugin) { + const instance = this.get(name); + if (instance) { + this.pluginAliases.delete(instance.name); + this.pluginInstances.delete(instance.constructor as typeof Plugin); + } + } + + async create(name: string | string[]) { + console.log('creating...'); + const pluginNames = Array.isArray(name) ? name : [name]; + const { run } = require('@nocobase/cli/src/util'); + const createPlugin = async (name) => { + const { PluginGenerator } = require('@nocobase/cli/src/plugin-generator'); + const generator = new PluginGenerator({ + cwd: resolve(process.cwd(), name), + args: {}, + context: { + name, + }, + }); + await generator.run(); + }; + await Promise.all(pluginNames.map((pluginName) => createPlugin(pluginName))); + await run('yarn', ['install']); + } + + async add(plugin?: any, options: any = {}) { + if (this.has(plugin)) { + const name = typeof plugin === 'string' ? plugin : plugin.name; + this.app.log.warn(`plugin [${name}] added`); + return; + } + if (!options.name && typeof plugin === 'string') { + options.name = plugin; + } + this.app.log.debug(`adding plugin [${options.name}]...`); + let P: any; + try { + P = PluginManager.resolvePlugin(plugin); + } catch (error) { + this.app.log.warn('plugin not found', error); + return; + } + const instance: Plugin = new P(createAppProxy(this.app), options); + this.pluginInstances.set(P, instance); + if (options.name) { + this.pluginAliases.set(options.name, instance); + } + await instance.afterAdd(); + } + + async initPlugins() { + await this.initPresetPlugins(); + await this.repository.init(); + } + + async load(options: any = {}) { + this.app.setMaintainingMessage('loading plugins...'); + const total = this.pluginInstances.size; + + let current = 0; + + for (const [P, plugin] of this.getPlugins()) { + if (plugin.state.loaded) { + continue; + } + + const name = P.name; + current += 1; + + this.app.setMaintainingMessage(`before load plugin [${name}], ${current}/${total}`); + if (!plugin.enabled) { + continue; + } + this.app.logger.debug(`before load plugin [${name}]...`); + await plugin.beforeLoad(); + } + + current = 0; + + for (const [P, plugin] of this.getPlugins()) { + if (plugin.state.loaded) { + continue; + } + const name = P.name; + current += 1; + this.app.setMaintainingMessage(`load plugin [${name}], ${current}/${total}`); + + if (!plugin.enabled) { + continue; + } + + await this.app.emitAsync('beforeLoadPlugin', plugin, options); + this.app.logger.debug(`loading plugin [${name}]...`); + await plugin.load(); + plugin.state.loaded = true; + await this.app.emitAsync('afterLoadPlugin', plugin, options); + this.app.logger.debug(`after load plugin [${name}]...`); + } + + this.app.setMaintainingMessage('loaded plugins'); + } + + async install(options: InstallOptions = {}) { + this.app.setMaintainingMessage('install plugins...'); + const total = this.pluginInstances.size; + let current = 0; + + this.app.log.debug('call db.sync()'); + await this.app.db.sync(); + const toBeUpdated = []; + + for (const [P, plugin] of this.getPlugins()) { + if (plugin.state.installing || plugin.state.installed) { + continue; + } + + const name = P.name; + current += 1; + + if (!plugin.enabled) { + continue; + } + + plugin.state.installing = true; + this.app.setMaintainingMessage(`before install plugin [${name}], ${current}/${total}`); + await this.app.emitAsync('beforeInstallPlugin', plugin, options); + this.app.logger.debug(`install plugin [${name}]...`); + await plugin.install(options); + toBeUpdated.push(name); + plugin.state.installing = false; + plugin.state.installed = true; + plugin.installed = true; + this.app.setMaintainingMessage(`after install plugin [${name}], ${current}/${total}`); + await this.app.emitAsync('afterInstallPlugin', plugin, options); + } + await this.repository.update({ + filter: { + name: toBeUpdated, + }, + values: { + installed: true, + }, + }); + } + + async enable(name: string | string[]) { + const pluginNames = _.castArray(name); + this.app.log.debug(`enabling plugin ${pluginNames.join(',')}`); + this.app.setMaintainingMessage(`enabling plugin ${pluginNames.join(',')}`); + const toBeUpdated = []; + for (const pluginName of pluginNames) { + const plugin = this.get(pluginName); + if (!plugin) { + throw new Error(`${pluginName} plugin does not exist`); + } + if (plugin.enabled) { + continue; + } + await this.app.emitAsync('beforeEnablePlugin', pluginName); + await plugin.beforeEnable(); + plugin.enabled = true; + toBeUpdated.push(pluginName); + } + if (toBeUpdated.length === 0) { + return; + } + await this.repository.update({ + filter: { + name: toBeUpdated, + }, + values: { + enabled: true, + }, + }); + try { + await this.app.reload(); + this.app.log.debug(`syncing database in enable plugin ${pluginNames.join(',')}...`); + this.app.setMaintainingMessage(`syncing database in enable plugin ${pluginNames.join(',')}...`); + await this.app.db.sync(); + for (const pluginName of pluginNames) { + const plugin = this.get(pluginName); + if (!plugin.installed) { + this.app.log.debug(`installing plugin ${pluginName}...`); + this.app.setMaintainingMessage(`installing plugin ${pluginName}...`); + await plugin.install(); + plugin.installed = true; + } + } + await this.repository.update({ + filter: { + name: toBeUpdated, + }, + values: { + installed: true, + }, + }); + for (const pluginName of pluginNames) { + const plugin = this.get(pluginName); + this.app.log.debug(`emit afterEnablePlugin event...`); + await plugin.afterEnable(); + await this.app.emitAsync('afterEnablePlugin', pluginName); + this.app.log.debug(`afterEnablePlugin event emitted`); + } + await this.app.tryReloadOrRestart(); + } catch (error) { + await this.repository.update({ + filter: { + name: toBeUpdated, + }, + values: { + enabled: false, + installed: false, + }, + }); + await this.app.tryReloadOrRestart(); + throw error; + } + } + + async disable(name: string | string[]) { + const pluginNames = _.castArray(name); + this.app.log.debug(`disabling plugin ${pluginNames.join(',')}`); + this.app.setMaintainingMessage(`disabling plugin ${pluginNames.join(',')}`); + const toBeUpdated = []; + for (const pluginName of pluginNames) { + const plugin = this.get(pluginName); + if (!plugin) { + throw new Error(`${pluginName} plugin does not exist`); + } + if (!plugin.enabled) { + continue; + } + await this.app.emitAsync('beforeDisablePlugin', pluginName); + await plugin.beforeDisable(); + plugin.enabled = false; + toBeUpdated.push(pluginName); + } + if (toBeUpdated.length === 0) { + return; + } + await this.repository.update({ + filter: { + name: toBeUpdated, + }, + values: { + enabled: false, + }, + }); + try { + await this.app.tryReloadOrRestart(); + for (const pluginName of pluginNames) { + const plugin = this.get(pluginName); + this.app.log.debug(`emit afterDisablePlugin event...`); + await plugin.afterDisable(); + await this.app.emitAsync('afterDisablePlugin', pluginName); + this.app.log.debug(`afterDisablePlugin event emitted`); + } + } catch (error) { + await this.repository.update({ + filter: { + name: toBeUpdated, + }, + values: { + enabled: true, + }, + }); + await this.app.tryReloadOrRestart(); + throw error; + } + } + + async remove(name: string | string[]) { + const pluginNames = _.castArray(name); + for (const pluginName of pluginNames) { + const plugin = this.get(pluginName); + if (!plugin) { + throw new Error(`${pluginName} plugin does not exist`); + } + if (plugin.enabled) { + throw new Error(`${pluginName} plugin is enabled`); + } + await plugin.beforeRemove(); + } + await this.repository.destroy({ + filter: { + name: pluginNames, + }, + }); + const plugins: Plugin[] = []; + for (const pluginName of pluginNames) { + const plugin = this.get(pluginName); + plugins.push(plugin); + this.del(pluginName); + } + await this.app.reload(); + for (const plugin of plugins) { + await plugin.afterRemove(); + } + } + + protected async initPresetPlugins() { + for (const plugin of this.options.plugins) { + const [p, opts = {}] = Array.isArray(plugin) ? plugin : [plugin]; + await this.add(p, { enabled: true, isPreset: true, ...opts }); + } } } diff --git a/packages/core/server/src/plugin.ts b/packages/core/server/src/plugin.ts index dc67ed4b9..51aabf19d 100644 --- a/packages/core/server/src/plugin.ts +++ b/packages/core/server/src/plugin.ts @@ -1,3 +1,4 @@ +import { Model } from '@nocobase/database'; import { Application } from './application'; import { InstallOptions } from './plugin-manager'; @@ -22,24 +23,29 @@ export interface PluginOptions { [key: string]: any; } -export type PluginType = typeof Plugin; - export abstract class Plugin implements PluginInterface { options: any; app: Application; + model: Model; + state: any = {}; constructor(app: Application, options?: any) { - this.setOptions(options); - this.app = app; this.setOptions(options); - this.afterAdd(); + } + + get log() { + return this.app.log; } get name() { return this.options.name as string; } + get pm() { + return this.app.pm; + } + get db() { return this.app.db; } @@ -52,6 +58,14 @@ export abstract class Plugin implements PluginInterface { this.options.enabled = value; } + get installed() { + return this.options.installed; + } + + set installed(value) { + this.options.installed = value; + } + setOptions(options: any) { this.options = options || {}; } @@ -72,9 +86,13 @@ export abstract class Plugin implements PluginInterface { async afterEnable() {} + async beforeDisable() {} + async afterDisable() {} - async remove() {} + async beforeRemove() {} + + async afterRemove() {} async importCollections(collectionsPath: string) { await this.db.import({ diff --git a/packages/core/test/package.json b/packages/core/test/package.json index 146434745..1f8e89e88 100644 --- a/packages/core/test/package.json +++ b/packages/core/test/package.json @@ -12,7 +12,8 @@ "pg": "^8.7.3", "pg-hstore": "^2.3.4", "sqlite3": "^5.0.8", - "supertest": "^6.1.6" + "supertest": "^6.1.6", + "ws": "^8.13.0" }, "gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644" } diff --git a/packages/core/test/src/index.ts b/packages/core/test/src/index.ts index ee8329b2c..54dc2078c 100644 --- a/packages/core/test/src/index.ts +++ b/packages/core/test/src/index.ts @@ -1,5 +1,8 @@ +import ws from 'ws'; + export { mockDatabase } from '@nocobase/database'; export * from './mockServer'; +export { default as supertest } from 'supertest'; export const pgOnly: () => jest.Describe = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip); @@ -7,3 +10,54 @@ export function randomStr() { // create random string return Math.random().toString(36).substring(2); } + +export const waitSecond = async () => { + await new Promise((resolve) => setTimeout(resolve, 1000)); +}; + +export const startServerWithRandomPort = async (startServer) => { + return await new Promise((resolve) => { + startServer({ + port: 0, + host: 'localhost', + callback(server) { + // @ts-ignore + const port = server.address().port; + resolve(port); + }, + }); + }); +}; + +export const createWsClient = async ({ serverPort, options = {} }) => { + console.log(`connect to ws://localhost:${serverPort}/ws`, options); + + const wsc = new ws(`ws://localhost:${serverPort}/ws`, options); + const messages = []; + + wsc.on('message', (data) => { + const message = data.toString(); + messages.push(message); + }); + + // await connection established + await new Promise((resolve) => { + wsc.on('open', resolve); + }); + + return { + wsc, + messages, + async stop() { + const promise = new Promise((resolve) => { + wsc.on('close', resolve); + }); + + wsc.close(); + await promise; + }, + lastMessage() { + return JSON.parse(messages[messages.length - 1]); + }, + }; +}; diff --git a/packages/core/test/src/mockServer.ts b/packages/core/test/src/mockServer.ts index 74d5c1638..835091f2a 100644 --- a/packages/core/test/src/mockServer.ts +++ b/packages/core/test/src/mockServer.ts @@ -1,5 +1,5 @@ import { Database, mockDatabase } from '@nocobase/database'; -import Application, { ApplicationOptions, PluginManager } from '@nocobase/server'; +import Application, { AppSupervisor, ApplicationOptions, Gateway, PluginManager } from '@nocobase/server'; import jwt from 'jsonwebtoken'; import qs from 'qs'; import supertest, { SuperAgentTest } from 'supertest'; @@ -81,12 +81,19 @@ export class MockServer extends Application { await this.db.clean({ drop: true }); } + async destroy(options: any = {}): Promise { + await super.destroy(options); + + Gateway.getInstance().destroy(); + await AppSupervisor.getInstance().destroy(); + } + agent(): SuperAgentTest & { login: (user: any) => SuperAgentTest; loginUsingId: (userId: number) => SuperAgentTest; resource: (name: string, resourceOf?: any) => Resource; } { - const agent = supertest.agent(this.appManager.callback()); + const agent = supertest.agent(this.callback()); const prefix = this.resourcer.options.prefix; const proxy = new Proxy(agent, { get(target, method: string, receiver) { @@ -116,7 +123,8 @@ export class MockServer extends Application { { get(target, method: string, receiver) { return (params: ActionParams = {}) => { - let { filterByTk, values = {}, file, ...restParams } = params; + let { filterByTk } = params; + const { values = {}, file, ...restParams } = params; if (params.associatedIndex) { resourceOf = params.associatedIndex; } @@ -172,6 +180,9 @@ export function mockServer(options: ApplicationOptions = {}) { global.TextDecoder = require('util').TextDecoder; } + Gateway.getInstance().reset(); + AppSupervisor.getInstance().reset(); + // @ts-ignore if (!PluginManager.findPackagePatched) { PluginManager.getPackageJson = () => { diff --git a/packages/plugins/acl/src/server/__tests__/prepare.ts b/packages/plugins/acl/src/server/__tests__/prepare.ts index dccfa6316..f6bcbb2bd 100644 --- a/packages/plugins/acl/src/server/__tests__/prepare.ts +++ b/packages/plugins/acl/src/server/__tests__/prepare.ts @@ -13,8 +13,6 @@ export async function prepareApp(): Promise { }); await app.loadAndInstall({ clean: true }); - - await app.db.sync(); - + await app.start(); return app; } diff --git a/packages/plugins/acl/src/server/__tests__/users.test.ts b/packages/plugins/acl/src/server/__tests__/users.test.ts index 9ac6a549e..1b8f7e35b 100644 --- a/packages/plugins/acl/src/server/__tests__/users.test.ts +++ b/packages/plugins/acl/src/server/__tests__/users.test.ts @@ -31,7 +31,7 @@ describe('actions', () => { }); afterEach(async () => { - await db.close(); + await app.destroy(); }); it('update profile with roles', async () => { diff --git a/packages/plugins/acl/src/server/server.ts b/packages/plugins/acl/src/server/server.ts index 87b65be9b..eca673b15 100644 --- a/packages/plugins/acl/src/server/server.ts +++ b/packages/plugins/acl/src/server/server.ts @@ -342,23 +342,17 @@ export class PluginACL extends Plugin { } }); - // sync database role data to acl - this.app.on('afterLoad', async (app, options) => { - if (options?.method === 'install' || options?.method === 'upgrade') { - return; - } + const writeRolesToACL = async (app, options) => { const exists = await this.app.db.collectionExistsInDb('roles'); if (exists) { + this.log.info('write roles to ACL'); await this.writeRolesToACL(); } - }); + }; - this.app.on('afterInstall', async (app, options) => { - const exists = await this.app.db.collectionExistsInDb('roles'); - if (exists) { - await this.writeRolesToACL(); - } - }); + // sync database role data to acl + this.app.on('afterLoad', writeRolesToACL); + this.app.on('afterInstall', writeRolesToACL); this.app.on('afterInstallPlugin', async (plugin) => { if (plugin.getName() !== 'users') { diff --git a/packages/plugins/api-keys/src/server/__tests__/actions.test.ts b/packages/plugins/api-keys/src/server/__tests__/actions.test.ts index 9c655746b..c4355f87d 100644 --- a/packages/plugins/api-keys/src/server/__tests__/actions.test.ts +++ b/packages/plugins/api-keys/src/server/__tests__/actions.test.ts @@ -26,7 +26,7 @@ describe('actions', () => { await repo.destroy({ truncate: true, }); - await db.close(); + await app.destroy(); }); let user; @@ -54,7 +54,7 @@ describe('actions', () => { }, }); - role = await (db.getRepository('users.roles', user.id) as unknown as Repository).findOne({ + role = await ((db.getRepository('users.roles', user.id) as unknown) as Repository).findOne({ where: { default: true, }, diff --git a/packages/plugins/audit-logs/src/server/__tests__/hook.test.ts b/packages/plugins/audit-logs/src/server/__tests__/hook.test.ts index 282ea0c16..69839accf 100644 --- a/packages/plugins/audit-logs/src/server/__tests__/hook.test.ts +++ b/packages/plugins/audit-logs/src/server/__tests__/hook.test.ts @@ -8,9 +8,8 @@ describe('hook', () => { beforeEach(async () => { api = mockServer(); - await api.db.clean({ drop: true }); api.plugin(logPlugin, { name: 'audit-logs' }); - await api.load(); + await api.loadAndInstall({ clean: true }); db = api.db; db.collection({ name: 'posts', diff --git a/packages/plugins/auth/src/server/__tests__/actions.test.ts b/packages/plugins/auth/src/server/__tests__/actions.test.ts index 6d3f6b20e..0fee16066 100644 --- a/packages/plugins/auth/src/server/__tests__/actions.test.ts +++ b/packages/plugins/auth/src/server/__tests__/actions.test.ts @@ -27,7 +27,7 @@ describe('actions', () => { }); afterAll(async () => { - await db.close(); + await app.destroy(); }); it('should list authenticator types', async () => { diff --git a/packages/plugins/auth/src/server/__tests__/token-blacklist.test.ts b/packages/plugins/auth/src/server/__tests__/token-blacklist.test.ts index c6400cf2d..2f6b85a57 100644 --- a/packages/plugins/auth/src/server/__tests__/token-blacklist.test.ts +++ b/packages/plugins/auth/src/server/__tests__/token-blacklist.test.ts @@ -19,7 +19,7 @@ describe('token-blacklist', () => { }); afterAll(async () => { - await db.close(); + await app.destroy(); }); afterEach(async () => { @@ -66,7 +66,6 @@ describe('token-blacklist', () => { token: 'should not be deleted', expiration: new Date('2100-01-01'), }); - await tokenBlacklist.deleteByExpiration(); expect(await tokenBlacklist.has('should be deleted')).not.toBeTruthy(); expect(await tokenBlacklist.has('should not be deleted')).toBeTruthy(); }); diff --git a/packages/plugins/auth/src/server/token-blacklist.ts b/packages/plugins/auth/src/server/token-blacklist.ts index cb7f20bac..8db0b5063 100644 --- a/packages/plugins/auth/src/server/token-blacklist.ts +++ b/packages/plugins/auth/src/server/token-blacklist.ts @@ -9,36 +9,12 @@ export class TokenBlacklistService implements ITokenBlacklistService { constructor(protected plugin: AuthPlugin) { this.repo = plugin.db.getRepository('tokenBlacklist'); - this.cronJob = this.createCronJob(); } get app() { return this.plugin.app; } - createCronJob() { - const cronJob = new CronJob( - // every day at 03:00 - '0 3 * * *', // - async () => { - this.app.logger.info(`${this.plugin.name}: Start delete expired blacklist token`); - await this.deleteByExpiration(); - this.app.logger.info(`${this.plugin.name}: End delete expired blacklist token`); - }, - null, - ); - - this.app.once('beforeStart', () => { - cronJob.start(); - }); - - this.app.once('beforeStop', () => { - cronJob.stop(); - }); - - return cronJob; - } - async has(token: string) { return !!(await this.repo.findOne({ filter: { @@ -46,7 +22,9 @@ export class TokenBlacklistService implements ITokenBlacklistService { }, })); } + async add(values) { + await this.deleteExpiredTokens(); return this.repo.model.findOrCreate({ defaults: values, where: { @@ -54,7 +32,8 @@ export class TokenBlacklistService implements ITokenBlacklistService { }, }); } - async deleteByExpiration() { + + async deleteExpiredTokens() { return this.repo.destroy({ filter: { expiration: { diff --git a/packages/plugins/china-region/src/server/__tests__/action.test.ts b/packages/plugins/china-region/src/server/__tests__/action.test.ts index 2b64bf109..2844fb701 100644 --- a/packages/plugins/china-region/src/server/__tests__/action.test.ts +++ b/packages/plugins/china-region/src/server/__tests__/action.test.ts @@ -10,11 +10,8 @@ describe('actions test', () => { registerActions: true, }); - await app.cleanDb(); - app.plugin(Plugin); - await app.load(); - await app.db.sync(); + await app.loadAndInstall({ clean: true }); db = app.db; }); diff --git a/packages/plugins/client/src/server/server.ts b/packages/plugins/client/src/server/server.ts index 09e9d755b..05f588820 100644 --- a/packages/plugins/client/src/server/server.ts +++ b/packages/plugins/client/src/server/server.ts @@ -1,8 +1,6 @@ import { Plugin, PluginManager, getPackageClientStaticUrl } from '@nocobase/server'; import fs from 'fs'; -import send from 'koa-send'; -import serve from 'koa-static'; -import { isAbsolute, resolve } from 'path'; +import { resolve } from 'path'; import { getAntdLocale } from './antd'; import { getCronLocale } from './cron'; import { getCronstrueLocale } from './cronstrue'; @@ -125,7 +123,7 @@ export class ClientPlugin extends Plugin { this.app.acl.allow('plugins', '*', 'public'); this.app.acl.registerSnippet({ name: 'app', - actions: ['app:reboot', 'app:clearCache'], + actions: ['app:restart', 'app:clearCache'], }); const dialect = this.app.db.sequelize.getDialect(); const restartMark = resolve(process.cwd(), 'storage', 'restart'); @@ -153,6 +151,7 @@ export class ClientPlugin extends Plugin { }, version: await ctx.app.version.get(), lang, + name: ctx.app.name, theme: currentUser?.systemSettings?.theme || systemSetting?.options?.theme || 'default', }; await next(); @@ -194,19 +193,9 @@ export class ClientPlugin extends Plugin { await ctx.cache.reset(); await next(); }, - reboot(ctx) { - const RESTART_CODE = 100; - process.on('exit', (code) => { - if (code === RESTART_CODE && process.env.APP_ENV === 'production') { - fs.writeFileSync(restartMark, '1'); - console.log('Restart mark created.'); - } - }); - ctx.app.on('afterStop', () => { - // Exit with code 100 will restart the process - process.exit(RESTART_CODE); - }); - ctx.app.stop(); + async restart(ctx, next) { + ctx.app.runAsCLI(['restart'], { from: 'user' }); + await next(); }, }, }); @@ -242,25 +231,6 @@ export class ClientPlugin extends Plugin { }, }, }); - let root = this.options.dist || `${process.env.APP_PACKAGE_ROOT}/dist/client`; - if (!isAbsolute(root)) { - root = resolve(process.cwd(), root); - } - if (process.env.APP_ENV !== 'production' && root) { - this.app.use( - async (ctx, next) => { - if (ctx.path.startsWith(this.app.resourcer.options.prefix)) { - return next(); - } - await serve(root)(ctx, next); - // console.log('koa-send', root, ctx.status); - if (ctx.status == 404) { - return send(ctx, 'index.html', { root }); - } - }, - { tag: 'clientStatic', before: 'cors' }, - ); - } } } diff --git a/packages/plugins/collection-manager/src/server/__tests__/index.ts b/packages/plugins/collection-manager/src/server/__tests__/index.ts index efd8376f5..bb26ce275 100644 --- a/packages/plugins/collection-manager/src/server/__tests__/index.ts +++ b/packages/plugins/collection-manager/src/server/__tests__/index.ts @@ -9,17 +9,17 @@ export async function createApp( const app = mockServer({ acl: false, ...options, + // plugins: ['error-handler', 'collection-manager', 'ui-schema-storage'], }); - await app.db.clean({ drop: true }); - await app.db.sync({}); - options.beforePlugin && options.beforePlugin(app); app.plugin(PluginErrorHandler, { name: 'error-handler' }); app.plugin(Plugin, { name: 'collection-manager' }); app.plugin(PluginUiSchema, { name: 'ui-schema-storage' }); + await app.load(); + if (options.beforeInstall) { await options.beforeInstall(app); } diff --git a/packages/plugins/collection-manager/src/server/__tests__/through.test.ts b/packages/plugins/collection-manager/src/server/__tests__/through.test.ts index 961acc632..df7bd16de 100644 --- a/packages/plugins/collection-manager/src/server/__tests__/through.test.ts +++ b/packages/plugins/collection-manager/src/server/__tests__/through.test.ts @@ -13,6 +13,7 @@ describe('collections repository', () => { app1.plugin(PluginErrorHandler, { name: 'error-handler' }); app1.plugin(Plugin, { name: 'collection-manager' }); await app1.loadAndInstall({ clean: true }); + await app1.start(); await app1 .agent() @@ -123,6 +124,7 @@ describe('collections repository', () => { app2.plugin(PluginErrorHandler, { name: 'error-handler' }); app2.plugin(Plugin, { name: 'collection-manager' }); await app2.load(); + await app2.start(); await app2.db.sync({ force: true, diff --git a/packages/plugins/collection-manager/src/server/models/field.ts b/packages/plugins/collection-manager/src/server/models/field.ts index 1071434c2..3aa16db4a 100644 --- a/packages/plugins/collection-manager/src/server/models/field.ts +++ b/packages/plugins/collection-manager/src/server/models/field.ts @@ -35,7 +35,10 @@ export class FieldModel extends MagicAttributeModel { const options = this.get(); - const field = collection.setField(name, options); + const field = await (async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + return collection.setField(name, options); + })(); await this.db.emitAsync('field:loaded', { fieldKey: this.get('key'), diff --git a/packages/plugins/collection-manager/src/server/repositories/collection-repository.ts b/packages/plugins/collection-manager/src/server/repositories/collection-repository.ts index 597437be4..ac4521076 100644 --- a/packages/plugins/collection-manager/src/server/repositories/collection-repository.ts +++ b/packages/plugins/collection-manager/src/server/repositories/collection-repository.ts @@ -1,4 +1,5 @@ import { Repository } from '@nocobase/database'; +import { Application } from '@nocobase/server'; import { CollectionsGraph } from '@nocobase/utils'; import lodash from 'lodash'; import { CollectionModel } from '../models/collection'; @@ -9,9 +10,17 @@ interface LoadOptions { } export class CollectionRepository extends Repository { + private app: Application; + + setApp(app) { + this.app = app; + } + async load(options: LoadOptions = {}) { const { filter, skipExist } = options; + console.log('start load collections'); const instances = (await this.find({ filter, appends: ['fields'] })) as CollectionModel[]; + console.log('end load collections'); const graphlib = CollectionsGraph.graphlib(); @@ -83,19 +92,27 @@ export class CollectionRepository extends Repository { if (lodash.isArray(skipField) && skipField.length) { lazyCollectionFields[instanceName] = skipField; } + this.database.logger.debug(`load ${instanceName} collection`); + this.app.setMaintainingMessage(`load ${instanceName} collection`); await nameMap[instanceName].load({ skipField }); } // load view fields for (const viewCollectionName of viewCollections) { + this.database.logger.debug(`load ${viewCollectionName} collection fields`); + this.app.setMaintainingMessage(`load ${viewCollectionName} collection fields`); await nameMap[viewCollectionName].loadFields({}); } // load lazy collection field for (const [collectionName, skipField] of Object.entries(lazyCollectionFields)) { + this.database.logger.debug(`load ${collectionName} collection fields`); + this.app.setMaintainingMessage(`load ${collectionName} collection fields`); await nameMap[collectionName].loadFields({ includeFields: skipField }); } + + console.log('finished load collection'); } async db2cm(collectionName: string) { diff --git a/packages/plugins/collection-manager/src/server/server.ts b/packages/plugins/collection-manager/src/server/server.ts index 1e7f501ad..261fc78a9 100644 --- a/packages/plugins/collection-manager/src/server/server.ts +++ b/packages/plugins/collection-manager/src/server/server.ts @@ -208,23 +208,24 @@ export class CollectionManagerPlugin extends Plugin { }); }); - this.app.on('afterLoad', async (app, options) => { - if (options?.method === 'install') { - return; - } - if (options?.method === 'upgrade') { - return; - } - const exists = await this.app.db.collectionExistsInDb('collections'); - if (exists) { - try { - await this.app.db.getRepository('collections').load(); - } catch (error) { - this.app.logger.warn(error); - await this.app.db.sync(); - await this.app.db.getRepository('collections').load(); - } - } + const loadCollections = async () => { + this.app.log.debug('loading custom collections'); + this.app.setMaintainingMessage('loading custom collections'); + await this.app.db.getRepository('collections').load(); + }; + + this.app.on('afterStart', loadCollections); + this.app.on('beforeUpgrade', async () => { + const syncOptions = { + alter: { + drop: false, + }, + force: false, + }; + await this.db.getCollection('collections').sync(syncOptions); + await this.db.getCollection('fields').sync(syncOptions); + await this.db.getCollection('collectionCategories').sync(syncOptions); + await loadCollections(); }); this.app.resourcer.use(async (ctx, next) => { @@ -245,8 +246,9 @@ export class CollectionManagerPlugin extends Plugin { async load() { await this.importCollections(path.resolve(__dirname, './collections')); + this.db.getRepository('collections').setApp(this.app); - const errorHandlerPlugin = this.app.getPlugin('error-handler'); + const errorHandlerPlugin = this.app.getPlugin('error-handler'); errorHandlerPlugin.errorHandler.register( (err) => { return err instanceof UniqueConstraintError; diff --git a/packages/plugins/data-visualization/src/server/__tests__/api.test.ts b/packages/plugins/data-visualization/src/server/__tests__/api.test.ts index 7b6e27d5e..52460b509 100644 --- a/packages/plugins/data-visualization/src/server/__tests__/api.test.ts +++ b/packages/plugins/data-visualization/src/server/__tests__/api.test.ts @@ -48,7 +48,7 @@ describe('api', () => { }); afterAll(async () => { - await db.close(); + await app.destroy(); }); test('query', async () => { diff --git a/packages/plugins/duplicator/src/server/app-migrator.ts b/packages/plugins/duplicator/src/server/app-migrator.ts index 4048f367a..0ede98347 100644 --- a/packages/plugins/duplicator/src/server/app-migrator.ts +++ b/packages/plugins/duplicator/src/server/app-migrator.ts @@ -42,7 +42,7 @@ abstract class AppMigrator extends EventEmitter { async getAppPlugins() { const plugins = await this.app.db.getCollection('applicationPlugins').repository.find(); - return lodash.uniq(['core', ...this.app.pm.plugins.keys(), ...plugins.map((plugin) => plugin.get('name'))]); + return lodash.uniq(['core', ...this.app.pm.getAliases(), ...plugins.map((plugin) => plugin.get('name'))]); } async getAppPluginCollectionGroups() { diff --git a/packages/plugins/duplicator/src/server/commands/dump-command.ts b/packages/plugins/duplicator/src/server/commands/dump-command.ts index 4ee944ce9..fc044eeb2 100644 --- a/packages/plugins/duplicator/src/server/commands/dump-command.ts +++ b/packages/plugins/duplicator/src/server/commands/dump-command.ts @@ -1,5 +1,5 @@ import inquirer from 'inquirer'; -import { Application } from '@nocobase/server'; +import { Application, AppSupervisor } from '@nocobase/server'; import { Dumper } from '../dumper'; import InquireQuestionBuilder from './inquire-question-builder'; @@ -11,7 +11,8 @@ export default function addDumpCommand(app: Application) { let dumpApp = app; if (options.app) { - const subApp = await app.appManager.getApplication(options.app); + const subApp = await AppSupervisor.getInstance().getApp(options.app); + if (!subApp) { app.log.error(`app ${options.app} not found`); await app.stop(); diff --git a/packages/plugins/duplicator/src/server/commands/restore-command.ts b/packages/plugins/duplicator/src/server/commands/restore-command.ts index defd54a28..985dbee3d 100644 --- a/packages/plugins/duplicator/src/server/commands/restore-command.ts +++ b/packages/plugins/duplicator/src/server/commands/restore-command.ts @@ -1,4 +1,4 @@ -import { Application } from '@nocobase/server'; +import { Application, AppSupervisor } from '@nocobase/server'; import { Restorer } from '../restorer'; import inquirer from 'inquirer'; import InquireQuestionBuilder from './inquire-question-builder'; @@ -26,7 +26,7 @@ export default function addRestoreCommand(app: Application) { }); } - const subApp = await app.appManager.getApplication(options.app); + const subApp = await AppSupervisor.getInstance().getApp(options.app); if (!subApp) { app.log.error(`app ${options.app} not found`); diff --git a/packages/plugins/error-handler/src/server/__tests__/render-error.test.ts b/packages/plugins/error-handler/src/server/__tests__/render-error.test.ts index b653c43db..acb904966 100644 --- a/packages/plugins/error-handler/src/server/__tests__/render-error.test.ts +++ b/packages/plugins/error-handler/src/server/__tests__/render-error.test.ts @@ -1,15 +1,15 @@ import { Database } from '@nocobase/database'; import { MockServer, mockServer } from '@nocobase/test'; -import supertest from 'supertest'; -import { PluginErrorHandler } from '../server'; describe('create with exception', () => { let app: MockServer; beforeEach(async () => { app = mockServer({ acl: false, + plugins: ['error-handler'], }); - await app.cleanDb(); - app.plugin(PluginErrorHandler, { name: 'error-handler' }); + // app.plugin(PluginErrorHandler, { name: 'error-handler' }); + await app.loadAndInstall({ clean: true }); + await app.start(); }); afterEach(async () => { @@ -17,7 +17,7 @@ describe('create with exception', () => { }); it('should handle not null error', async () => { - app.collection({ + const collection = app.collection({ name: 'users', fields: [ { @@ -28,7 +28,7 @@ describe('create with exception', () => { ], }); - await app.loadAndInstall(); + await collection.sync(); const response = await app .agent() @@ -51,7 +51,7 @@ describe('create with exception', () => { }); it('should handle unique error', async () => { - app.collection({ + const collection = app.collection({ name: 'users', fields: [ { @@ -62,7 +62,7 @@ describe('create with exception', () => { ], }); - await app.loadAndInstall(); + await collection.sync(); await app .agent() @@ -94,7 +94,7 @@ describe('create with exception', () => { }); it('should render error with field title', async () => { - app.collection({ + const collection = app.collection({ name: 'users', fields: [ { @@ -108,7 +108,7 @@ describe('create with exception', () => { ], }); - await app.loadAndInstall(); + await collection.sync(); const response = await app.agent().resource('users').create({}); @@ -137,7 +137,7 @@ describe('create with exception', () => { ], }); - await app.loadAndInstall(); + await userCollection.sync(); app.resourcer.define({ name: 'test', @@ -156,7 +156,7 @@ describe('create with exception', () => { }, }); - const agent = supertest.agent(app.callback()); + const agent = app.agent(); await agent.post('/test:test').send({ name: 'u1', diff --git a/packages/plugins/export/src/server/__tests__/utils/utils.test.ts b/packages/plugins/export/src/server/__tests__/utils/utils.test.ts index 6ed2503b3..8cb68302b 100644 --- a/packages/plugins/export/src/server/__tests__/utils/utils.test.ts +++ b/packages/plugins/export/src/server/__tests__/utils/utils.test.ts @@ -10,7 +10,10 @@ describe('utils', () => { app = mockServer(); db = app.db; }); - afterEach(async () => {}); + + afterEach(async () => { + await app.destroy(); + }); it('first columns2Appends', async () => { columns = [ @@ -27,8 +30,6 @@ describe('utils', () => { { dataIndex: ['f_qhvvfuignh2', 'createdBy', 'id'], defaultTitle: 'ID' }, { dataIndex: ['f_wu28mus1c65', 'roles', 'title'], defaultTitle: '角色名称' }, ]; - // const appends = columns2Appends(columns, app); - // expect(appends).toMatchObject(['f_qhvvfuignh2.createdBy', 'f_wu28mus1c65.roles']); }); it('second columns2Appends', async () => { @@ -46,7 +47,5 @@ describe('utils', () => { { dataIndex: ['f_qhvvfuignh2', 'createdBy', 'id'], defaultTitle: 'ID' }, { dataIndex: ['f_qhvvfuignh2', 'createdBy', 'nickname'], defaultTitle: '角色名称' }, ]; - // const appends = columns2Appends(columns, app); - // expect(appends).toMatchObject(['f_qhvvfuignh2.createdBy']); }); }); diff --git a/packages/plugins/file-manager/src/client/StorageOptions.tsx b/packages/plugins/file-manager/src/client/StorageOptions.tsx index b70062e0a..361568bac 100644 --- a/packages/plugins/file-manager/src/client/StorageOptions.tsx +++ b/packages/plugins/file-manager/src/client/StorageOptions.tsx @@ -12,14 +12,7 @@ const schema = { type: 'string', 'x-decorator': 'FormItem', 'x-component': 'Input', - default: 'uploads', - }, - serve: { - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Checkbox', - 'x-content': `{{t("Use the built-in static file server", { ns: "${NAMESPACE}" })}}`, - default: true, + default: 'storage/uploads', }, }, }, diff --git a/packages/plugins/file-manager/src/server/__tests__/action.test.ts b/packages/plugins/file-manager/src/server/__tests__/action.test.ts index 3c019c351..9d256774a 100644 --- a/packages/plugins/file-manager/src/server/__tests__/action.test.ts +++ b/packages/plugins/file-manager/src/server/__tests__/action.test.ts @@ -37,7 +37,7 @@ describe('action', () => { }); afterEach(async () => { - await db.close(); + await app.destroy(); }); describe('create / upload', () => { @@ -153,7 +153,7 @@ describe('action', () => { }); it('upload to storage which is not default', async () => { - const BASE_URL = `http://localhost:${APP_PORT}/another-uploads`; + const BASE_URL = `http://localhost:${APP_PORT}/storage/uploads/another`; const urlPath = 'test/path'; // 动态添加 storage @@ -167,7 +167,7 @@ describe('action', () => { path: urlPath, baseUrl: BASE_URL, options: { - documentRoot: 'uploads/another', + documentRoot: 'storage/uploads/another', }, }, }); diff --git a/packages/plugins/file-manager/src/server/__tests__/index.ts b/packages/plugins/file-manager/src/server/__tests__/index.ts index d7b1853b3..a592a2e11 100644 --- a/packages/plugins/file-manager/src/server/__tests__/index.ts +++ b/packages/plugins/file-manager/src/server/__tests__/index.ts @@ -1,4 +1,5 @@ import { MockServer, mockServer } from '@nocobase/test'; +import send from 'koa-send'; import path from 'path'; import supertest from 'supertest'; @@ -13,6 +14,14 @@ export async function getApp(options = {}): Promise { acl: false, }); + app.use(async (ctx, next) => { + if (ctx.path.startsWith('/storage/uploads')) { + await send(ctx, ctx.path, { root: process.cwd() }); + return; + } + await next(); + }); + await app.cleanDb(); app.plugin(plugin); diff --git a/packages/plugins/file-manager/src/server/__tests__/storages/ali-oss.test.ts b/packages/plugins/file-manager/src/server/__tests__/storages/ali-oss.test.ts index d2ca58dcb..52b3ebaa3 100644 --- a/packages/plugins/file-manager/src/server/__tests__/storages/ali-oss.test.ts +++ b/packages/plugins/file-manager/src/server/__tests__/storages/ali-oss.test.ts @@ -34,7 +34,7 @@ describe('storage:ali-oss', () => { }); afterEach(async () => { - await db.close(); + await app.destroy(); }); describe('upload', () => { diff --git a/packages/plugins/file-manager/src/server/__tests__/storages/s3.test.ts b/packages/plugins/file-manager/src/server/__tests__/storages/s3.test.ts index ee59c5b38..4d2e29e85 100644 --- a/packages/plugins/file-manager/src/server/__tests__/storages/s3.test.ts +++ b/packages/plugins/file-manager/src/server/__tests__/storages/s3.test.ts @@ -34,7 +34,7 @@ describe('storage:s3', () => { }); afterEach(async () => { - await db.close(); + await app.destroy(); }); describe('direct attachment', () => { diff --git a/packages/plugins/file-manager/src/server/__tests__/storages/tx-cos.test.ts b/packages/plugins/file-manager/src/server/__tests__/storages/tx-cos.test.ts index 2358dbe9d..5fd101570 100644 --- a/packages/plugins/file-manager/src/server/__tests__/storages/tx-cos.test.ts +++ b/packages/plugins/file-manager/src/server/__tests__/storages/tx-cos.test.ts @@ -28,7 +28,7 @@ describe('storage:tx-cos', () => { }); afterEach(async () => { - await db.close(); + await app.destroy(); }); describe('direct attachment', () => { diff --git a/packages/plugins/file-manager/src/server/server.ts b/packages/plugins/file-manager/src/server/server.ts index 37aa47d83..fcc24f50b 100644 --- a/packages/plugins/file-manager/src/server/server.ts +++ b/packages/plugins/file-manager/src/server/server.ts @@ -1,7 +1,6 @@ import { Plugin } from '@nocobase/server'; import { resolve } from 'path'; import initActions from './actions'; -import { STORAGE_TYPE_LOCAL } from './constants'; import { getStorageConfig } from './storages'; export { default as storageTypes } from './storages'; @@ -52,10 +51,6 @@ export default class PluginFileManager extends Plugin { // this.app.resourcer.use(createAction); // this.app.resourcer.registerActionHandler('upload', uploadAction); - if (process.env.APP_ENV !== 'production') { - await getStorageConfig(STORAGE_TYPE_LOCAL).middleware!(this.app); - } - const defaultStorageName = getStorageConfig(this.storageType()).defaults().name; this.app.acl.addFixedParams('storages', 'destroy', () => { diff --git a/packages/plugins/file-manager/src/server/storages/local.ts b/packages/plugins/file-manager/src/server/storages/local.ts index 7c7dd58ea..65c34e259 100644 --- a/packages/plugins/file-manager/src/server/storages/local.ts +++ b/packages/plugins/file-manager/src/server/storages/local.ts @@ -1,122 +1,18 @@ -import { Transactionable } from '@nocobase/database'; -import Application from '@nocobase/server'; import fs from 'fs/promises'; -import serve from 'koa-static'; import mkdirp from 'mkdirp'; import multer from 'multer'; import path from 'path'; -import { URL } from 'url'; import { AttachmentModel } from '.'; import { STORAGE_TYPE_LOCAL } from '../constants'; import { getFilename } from '../utils'; -// use koa-mount match logic -function match(basePath: string, pathname: string): boolean { - if (!pathname.startsWith(basePath)) { - return false; - } - - const newPath = pathname.replace(basePath, '') || '/'; - if (basePath.slice(-1) === '/') { - return true; - } - - return newPath[0] === '/'; -} - -async function refresh(app: Application, storages, options?: Transactionable) { - const Storage = app.db.getCollection('storages'); - - const items = await Storage.repository.find({ - filter: { - type: STORAGE_TYPE_LOCAL, - }, - transaction: options?.transaction, - }); - - const primaryKey = Storage.model.primaryKeyAttribute; - - storages.clear(); - for (const storage of items) { - storages.set(storage[primaryKey], storage); - } -} - -function createLocalServerUpdateHook(app, storages) { - return async function (row, options) { - if (row.get('type') === STORAGE_TYPE_LOCAL) { - await refresh(app, storages, options); - } - }; -} - function getDocumentRoot(storage): string { const { documentRoot = process.env.LOCAL_STORAGE_DEST || 'storage/uploads' } = storage.options || {}; // TODO(feature): 后面考虑以字符串模板的方式使用,可注入 req/action 相关变量,以便于区分文件夹 return path.resolve(path.isAbsolute(documentRoot) ? documentRoot : path.join(process.cwd(), documentRoot)); } -async function middleware(app: Application) { - const Storage = app.db.getCollection('storages'); - const storages = new Map(); - - const localServerUpdateHook = createLocalServerUpdateHook(app, storages); - Storage.model.addHook('afterSave', localServerUpdateHook); - Storage.model.addHook('afterDestroy', localServerUpdateHook); - - app.on('beforeStart', async () => { - await refresh(app, storages); - }); - - app.use(async function (ctx, next) { - for (const storage of storages.values()) { - const baseUrl = storage.get('baseUrl').trim(); - if (!baseUrl) { - console.error('"baseUrl" is not configured'); - // return ctx.throw(500); - continue; - } - - let url; - try { - url = new URL(baseUrl); - } catch (e) { - url = { - pathname: baseUrl, - }; - } - - // 以下情况才认为当前进程所应该提供静态服务 - // 否则都忽略,交给其他 server 来提供(如 nginx/cdn 等) - if (url.origin && storage?.options?.serve === false) { - continue; - } - - const basePath = url.pathname.startsWith('/') ? url.pathname : `/${url.pathname}`; - - if (!match(basePath, ctx.path)) { - continue; - } - - ctx.path = ctx.path.replace(basePath, ''); - - const documentRoot = getDocumentRoot(storage); - - return serve(documentRoot)(ctx, async () => { - if (ctx.status == 404) { - return; - } - - await next(); - }); - } - - await next(); - }); -} - export default { - middleware, make(storage) { return multer.diskStorage({ destination: function (req, file, cb) { diff --git a/packages/plugins/localization-management/src/server/__tests__/actions.test.ts b/packages/plugins/localization-management/src/server/__tests__/actions.test.ts index 0deac468c..79d612c48 100644 --- a/packages/plugins/localization-management/src/server/__tests__/actions.test.ts +++ b/packages/plugins/localization-management/src/server/__tests__/actions.test.ts @@ -25,11 +25,12 @@ describe('actions', () => { db = app.db; repo = db.getRepository('localizationTexts'); + await app.start(); agent = app.agent(); }); afterAll(async () => { - await db.close(); + await app.destroy(); }); describe('list', () => { diff --git a/packages/plugins/localization-management/src/server/plugin.ts b/packages/plugins/localization-management/src/server/plugin.ts index 98e1ec7f2..a23cd6c78 100644 --- a/packages/plugins/localization-management/src/server/plugin.ts +++ b/packages/plugins/localization-management/src/server/plugin.ts @@ -99,16 +99,8 @@ export class LocalizationManagementPlugin extends Plugin { this.resources = new Resources(this.db); - // ui-schema-storage loaded before localization-management this.registerUISchemahook(); - this.app.on('afterLoadPlugin', async (plugin) => { - if (plugin.name === 'ui-schema-storage') { - // ui-schema-storage loaded after localization-management - this.registerUISchemahook(plugin); - } - }); - this.app.resourcer.use(async (ctx, next) => { await next(); const { resourceName, actionName } = ctx.action.params; diff --git a/packages/plugins/multi-app-manager/src/client/settings/schemas/applications.ts b/packages/plugins/multi-app-manager/src/client/settings/schemas/applications.ts index 16fb8575c..e84bcd5b9 100644 --- a/packages/plugins/multi-app-manager/src/client/settings/schemas/applications.ts +++ b/packages/plugins/multi-app-manager/src/client/settings/schemas/applications.ts @@ -57,8 +57,13 @@ const collection = { type: 'string', title: i18nText('App status'), enum: [ - { label: 'Pending', value: 'pending' }, + { label: 'Initializing', value: 'initializing' }, + { label: 'Initialized', value: 'initialized' }, { label: 'Running', value: 'running' }, + { label: 'Commanding', value: 'commanding' }, + { label: 'Stopped', value: 'stopped' }, + { label: 'Error', value: 'error' }, + { label: 'Not found', value: 'not_found' }, ], 'x-component': 'Radio.Group', }, @@ -353,6 +358,18 @@ export const schema: ISchema = { }, }, }, + status: { + type: 'void', + 'x-decorator': 'Table.Column.Decorator', + 'x-component': 'Table.Column', + properties: { + status: { + type: 'string', + 'x-component': 'CollectionField', + 'x-read-pretty': true, + }, + }, + }, actions: { type: 'void', title: '{{t("Actions")}}', diff --git a/packages/plugins/multi-app-manager/src/server/__tests__/gateway.test.ts b/packages/plugins/multi-app-manager/src/server/__tests__/gateway.test.ts new file mode 100644 index 000000000..06d8583e2 --- /dev/null +++ b/packages/plugins/multi-app-manager/src/server/__tests__/gateway.test.ts @@ -0,0 +1,76 @@ +import { AppSupervisor, Gateway } from '@nocobase/server'; +import { createWsClient, MockServer, mockServer, startServerWithRandomPort, waitSecond } from '@nocobase/test'; +import { uid } from '@nocobase/utils'; +import { PluginMultiAppManager } from '../server'; + +describe('gateway with multiple apps', () => { + let app: MockServer; + let gateway: Gateway; + let wsClient; + + beforeEach(async () => { + gateway = Gateway.getInstance(); + + app = mockServer(); + await app.cleanDb(); + app.plugin(PluginMultiAppManager); + + await app.runCommand('install'); + }); + + afterEach(async () => { + if (wsClient) { + await wsClient.stop(); + } + + await app.destroy(); + }); + + it('should boot main app with sub apps', async () => { + const mainStatus = AppSupervisor.getInstance().getAppStatus('main'); + expect(mainStatus).toEqual('initialized'); + + const subAppName = `td_${uid()}`; + + // create app instance + await app.db.getRepository('applications').create({ + values: { + name: subAppName, + options: { + plugins: [], + }, + }, + context: { + waitSubAppInstall: true, + }, + }); + + const subApp = await AppSupervisor.getInstance().getApp(subAppName); + await subApp.destroy(); + + // start gateway + const port = await startServerWithRandomPort(gateway.startHttpServer.bind(gateway)); + + // create ws client + wsClient = await createWsClient({ + serverPort: port, + + options: { + headers: { + 'x-app': subAppName, + }, + }, + }); + + await waitSecond(); + console.log(wsClient.messages); + const lastMessage = wsClient.lastMessage(); + + expect(lastMessage).toMatchObject({ + type: 'maintaining', + payload: { + code: 'APP_RUNNING', + }, + }); + }); +}); diff --git a/packages/plugins/multi-app-manager/src/server/__tests__/mock-get-schema.test.ts b/packages/plugins/multi-app-manager/src/server/__tests__/mock-get-schema.test.ts index d9bd45fd5..7e6030216 100644 --- a/packages/plugins/multi-app-manager/src/server/__tests__/mock-get-schema.test.ts +++ b/packages/plugins/multi-app-manager/src/server/__tests__/mock-get-schema.test.ts @@ -1,4 +1,4 @@ -import { Plugin, PluginManager } from '@nocobase/server'; +import { AppSupervisor, Plugin, PluginManager } from '@nocobase/server'; import { mockServer } from '@nocobase/test'; import { uid } from '@nocobase/utils'; import { PluginMultiAppManager } from '../server'; @@ -22,16 +22,20 @@ describe('test with start', () => { } } - const mockGetPluginByName = jest.fn(); - mockGetPluginByName.mockReturnValue(TestPlugin); - PluginManager.resolvePlugin = mockGetPluginByName; + const resolvePlugin = PluginManager.resolvePlugin; + + PluginManager.resolvePlugin = (name) => { + if (name === 'test-package') { + return TestPlugin; + } + return resolvePlugin(name); + }; const app = mockServer(); - await app.cleanDb(); app.plugin(PluginMultiAppManager); - await app.loadAndInstall(); + await app.loadAndInstall({ clean: true }); await app.start(); const db = app.db; @@ -45,22 +49,25 @@ describe('test with start', () => { plugins: ['test-package'], }, }, + context: { + waitSubAppInstall: true, + }, }); expect(loadFn).toHaveBeenCalled(); expect(installFn).toHaveBeenCalledTimes(1); - const subApp = await app.appManager.getApplication(name); + const subApp = await AppSupervisor.getInstance().getApp(name); await subApp.destroy(); await app.destroy(); + PluginManager.resolvePlugin = resolvePlugin; }); it('should install into difference database', async () => { const app = mockServer(); - await app.cleanDb(); app.plugin(PluginMultiAppManager); - await app.loadAndInstall(); + await app.loadAndInstall({ clean: true }); await app.start(); const db = app.db; @@ -74,73 +81,12 @@ describe('test with start', () => { plugins: ['ui-schema-storage'], }, }, + context: { + waitSubAppInstall: true, + }, }); - const subApp = await app.appManager.getApplication(name); + const subApp = await AppSupervisor.getInstance().getApp(name); await subApp.destroy(); await app.destroy(); }); - - it('should lazy load applications', async () => { - class TestPlugin extends Plugin { - getName(): string { - return 'test-package'; - } - } - - const app = mockServer(); - await app.cleanDb(); - - app.plugin(PluginMultiAppManager); - - await app.loadAndInstall(); - await app.start(); - - const db = app.db; - - const mockGetPluginByName = jest.fn(); - mockGetPluginByName.mockReturnValue(TestPlugin); - PluginManager.resolvePlugin = mockGetPluginByName; - - const name = `d_${uid()}`; - console.log(name); - - await db.getRepository('applications').create({ - values: { - name, - options: { - plugins: ['test-package'], - }, - }, - }); - - expect(app.appManager.applications.get(name)).toBeDefined(); - - await app.appManager.applications.get(name).destroy(); - await app.stop(); - - const newApp = mockServer({ - database: app.db, - }); - - newApp.plugin(PluginMultiAppManager); - await newApp.db.reconnect(); - - await newApp.load(); - await newApp.start(); - - expect(await newApp.db.getRepository('applications').count()).toEqual(1); - expect(newApp.appManager.applications.get(name)).not.toBeDefined(); - - newApp.appManager.setAppSelector(() => { - return name; - }); - - await newApp.agent().resource('test').test(); - expect(newApp.appManager.applications.get(name)).toBeDefined(); - - await newApp.appManager.applications.get(name).destroy(); - - await newApp.destroy(); - await app.destroy(); - }); }); diff --git a/packages/plugins/multi-app-manager/src/server/__tests__/multiple-apps.test.ts b/packages/plugins/multi-app-manager/src/server/__tests__/multiple-apps.test.ts index 0668dd2ca..0103d9819 100644 --- a/packages/plugins/multi-app-manager/src/server/__tests__/multiple-apps.test.ts +++ b/packages/plugins/multi-app-manager/src/server/__tests__/multiple-apps.test.ts @@ -1,9 +1,12 @@ import { Database } from '@nocobase/database'; -import { mockServer, MockServer } from '@nocobase/test'; +import { AppSupervisor, Gateway } from '@nocobase/server'; +import { MockServer, mockServer } from '@nocobase/test'; import { uid } from '@nocobase/utils'; import { PluginMultiAppManager } from '../server'; -describe('multiple apps create', () => { +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +describe('multiple apps', () => { let app: MockServer; let db: Database; @@ -13,7 +16,8 @@ describe('multiple apps create', () => { await app.cleanDb(); app.plugin(PluginMultiAppManager); - await app.loadAndInstall(); + await app.runCommand('install'); + await app.runCommand('start'); }); afterEach(async () => { @@ -23,7 +27,7 @@ describe('multiple apps create', () => { it('should register db creator', async () => { const fn = jest.fn(); - const appPlugin = app.getPlugin('PluginMultiAppManager'); + const appPlugin = app.getPlugin(PluginMultiAppManager); const defaultDbCreator = appPlugin.appDbCreator; appPlugin.setAppDbCreator(async (app) => { @@ -32,6 +36,7 @@ describe('multiple apps create', () => { }); const name = `td_${uid()}`; + await db.getRepository('applications').create({ values: { name, @@ -39,24 +44,79 @@ describe('multiple apps create', () => { plugins: [], }, }, + context: { + waitSubAppInstall: true, + }, }); - await app.appManager.removeApplication(name); + + await AppSupervisor.getInstance().removeApp(name); expect(fn).toBeCalled(); }); it('should create application', async () => { const name = `td_${uid()}`; - const miniApp = await db.getRepository('applications').create({ + + await db.getRepository('applications').create({ values: { name, options: { plugins: [], }, }, + context: { + waitSubAppInstall: true, + }, }); - expect(app.appManager.applications.get(name)).toBeDefined(); + const subAppStatus = AppSupervisor.getInstance().getAppStatus(name); + expect(subAppStatus).toEqual('running'); + }); + + it('should list application with status', async () => { + const sub1 = `td_${uid()}`; + await db.getRepository('applications').create({ + values: { + name: sub1, + options: { + plugins: [], + }, + }, + context: { + waitSubAppInstall: true, + }, + }); + + await AppSupervisor.getInstance().removeApp(sub1); + + await sleep(1000); + + const expectStatus = async (appName, status) => { + const { body } = await app.agent().resource('applications').list(); + const { data } = body; + + const subApp = data.find((item) => item.name === appName); + expect(subApp.status).toEqual(status); + }; + + await expectStatus(sub1, 'stopped'); + + // start sub1 + // const startResponse = await app.agent().resource('applications').send({ + // action: 'start', + // appName: sub1, + // }); + + // expect(startResponse.statusCode).toEqual(200); + + // await expectStatus(sub1, 'started'); + + // const stopResponse = await app.agent().resource('applications').send({ + // action: 'stop', + // appName: 'sub1', + // }); + // + // await expectStatus('sub1', 'stopped'); }); it('should remove application', async () => { @@ -68,9 +128,12 @@ describe('multiple apps create', () => { plugins: [], }, }, + context: { + waitSubAppInstall: true, + }, }); - expect(app.appManager.applications.get(name)).toBeDefined(); + expect(AppSupervisor.getInstance().hasApp(name)).toBeTruthy(); await db.getRepository('applications').destroy({ filter: { @@ -78,7 +141,7 @@ describe('multiple apps create', () => { }, }); - expect(app.appManager.applications.get(name)).toBeUndefined(); + expect(AppSupervisor.getInstance().hasApp(name)).toBeFalsy(); }); it('should create with plugins', async () => { @@ -90,9 +153,12 @@ describe('multiple apps create', () => { plugins: [['ui-schema-storage', { test: 'B' }]], }, }, + context: { + waitSubAppInstall: true, + }, }); - const miniApp = app.appManager.applications.get(name); + const miniApp = await AppSupervisor.getInstance().getApp(name); expect(miniApp).toBeDefined(); const plugin = miniApp.pm.get('ui-schema-storage'); @@ -105,6 +171,8 @@ describe('multiple apps create', () => { it('should lazy load applications', async () => { const name = `td_${uid()}`; + + // create app instance await db.getRepository('applications').create({ values: { name, @@ -112,19 +180,21 @@ describe('multiple apps create', () => { plugins: ['ui-schema-storage'], }, }, + context: { + waitSubAppInstall: true, + }, }); - await app.appManager.removeApplication(name); + // remove it from supervisor + await AppSupervisor.getInstance().removeApp(name); - app.appManager.setAppSelector(() => { - return name; - }); + expect(AppSupervisor.getInstance().hasApp(name)).toBeFalsy(); - expect(app.appManager.applications.has(name)).toBeFalsy(); + Gateway.getInstance().appSelector = () => name; - await app.agent().resource('test').test(); + await AppSupervisor.getInstance().getApp(name); - expect(app.appManager.applications.has(name)).toBeTruthy(); + expect(AppSupervisor.getInstance().hasApp(name)).toBeTruthy(); }); it('should upgrade sub apps when main app upgrade', async () => { @@ -137,18 +207,27 @@ describe('multiple apps create', () => { plugins: [], }, }, + context: { + waitSubAppInstall: true, + }, }); - const subApp = await app.appManager.getApplication(subAppName); + await AppSupervisor.getInstance().removeApp(subAppName); + const jestFn = jest.fn(); - subApp.on('afterUpgrade', () => { - jestFn(); + AppSupervisor.getInstance().on('afterAppAdded', (subApp) => { + subApp.on('afterUpgrade', () => { + jestFn(); + }); }); - await app.upgrade(); + await app.runCommand('upgrade'); expect(jestFn).toBeCalled(); + + // sub app should remove after upgrade + expect(AppSupervisor.getInstance().hasApp(subAppName)).toBeFalsy(); }); it('should start automatically', async () => { @@ -157,38 +236,58 @@ describe('multiple apps create', () => { const subApp = await app.db.getRepository('applications').create({ values: { name: subAppName, - options: {}, + options: { + plugins: [], + }, + }, + context: { + waitSubAppInstall: true, }, }); - await app.appManager.removeApplication(subAppName); + await AppSupervisor.getInstance().removeApp(subAppName); + await app.start(); - expect(app.appManager.applications.get(subAppName)).toBeUndefined(); + + expect(AppSupervisor.getInstance().hasApp(subAppName)).toBeFalsy(); + await subApp.update({ options: { autoStart: true, }, }); - await app.appManager.removeApplication(subAppName); + + await AppSupervisor.getInstance().removeApp(subAppName); + + expect(AppSupervisor.getInstance().hasApp(subAppName)).toBeFalsy(); + await app.stop(); + await app.start(); - expect(app.appManager.applications.get(subAppName)).toBeDefined(); + + expect(AppSupervisor.getInstance().hasApp(subAppName)).toBeTruthy(); }); it('should get same obj ref when asynchronously access with same sub app name', async () => { const subAppName = `t_${uid()}`; - const subApp = await app.db.getRepository('applications').create({ + await app.db.getRepository('applications').create({ values: { name: subAppName, - options: {}, + options: { + plugins: [], + }, + }, + context: { + waitSubAppInstall: true, }, }); - await app.appManager.removeApplication(subAppName); - expect(app.appManager.applications.get(subAppName)).toBeUndefined(); + await AppSupervisor.getInstance().removeApp(subAppName); + + expect(AppSupervisor.getInstance().hasApp(subAppName)).toBeFalsy(); const instances = []; - app.on('afterSubAppAdded', (subApp) => { + AppSupervisor.getInstance().on('afterAppAdded', (subApp) => { instances.push(subApp); }); @@ -196,7 +295,7 @@ describe('multiple apps create', () => { for (let i = 0; i < 3; i++) { promises.push( (async () => { - await app.appManager.getApplication(subAppName); + await AppSupervisor.getInstance().getApp(subAppName); })(), ); } @@ -204,6 +303,6 @@ describe('multiple apps create', () => { expect(instances.length).toBe(1); expect(instances[0]).toBeDefined(); - expect(instances[0].name == subAppName).toBeTruthy(); + expect(instances[0].name).toEqual(subAppName); }); }); diff --git a/packages/plugins/multi-app-manager/src/server/models/application.ts b/packages/plugins/multi-app-manager/src/server/models/application.ts index c53d0608b..48b1fe389 100644 --- a/packages/plugins/multi-app-manager/src/server/models/application.ts +++ b/packages/plugins/multi-app-manager/src/server/models/application.ts @@ -8,7 +8,7 @@ export interface registerAppOptions extends Transactionable { } export class ApplicationModel extends Model { - registerToMainApp(mainApp: Application, options: registerAppOptions) { + registerToSupervisor(mainApp: Application, options: registerAppOptions) { const appName = this.get('name') as string; const appOptions = (this.get('options') as any) || {}; @@ -18,11 +18,6 @@ export class ApplicationModel extends Model { name: appName, }; - const subApp = new Application(subAppOptions); - - mainApp.appManager.addSubApp(subApp); - - console.log(`register application ${appName} to main app`); - return subApp; + return new Application(subAppOptions); } } diff --git a/packages/plugins/multi-app-manager/src/server/server.ts b/packages/plugins/multi-app-manager/src/server/server.ts index 35d3b62ce..f0a67ad88 100644 --- a/packages/plugins/multi-app-manager/src/server/server.ts +++ b/packages/plugins/multi-app-manager/src/server/server.ts @@ -1,10 +1,11 @@ -import Database, { IDatabaseOptions, Transactionable } from '@nocobase/database'; -import Application, { AppManager, Plugin } from '@nocobase/server'; -import lodash from 'lodash'; -import * as path from 'path'; -import { resolve } from 'path'; -import { ApplicationModel } from './models/application'; +import { Database, IDatabaseOptions, Transactionable } from '@nocobase/database'; +import Application, { AppSupervisor, Gateway, Plugin } from '@nocobase/server'; import { Mutex } from 'async-mutex'; +import lodash from 'lodash'; +import path, { resolve } from 'path'; +import qs from 'qs'; +import { parse } from 'url'; +import { ApplicationModel } from '../server'; export type AppDbCreator = (app: Application, transaction?: Transactionable) => Promise; export type AppOptionsFactory = (appName: string, mainApp: Application) => any; @@ -35,7 +36,9 @@ const defaultDbCreator = async (app: Application) => { try { await client.query(`CREATE DATABASE "${database}"`); - } catch (e) {} + } catch (e) { + console.log(e); + } await client.end(); } @@ -72,14 +75,6 @@ export class PluginMultiAppManager extends Plugin { private beforeGetApplicationMutex = new Mutex(); - setAppOptionsFactory(factory: AppOptionsFactory) { - this.appOptionsFactory = factory; - } - - setAppDbCreator(appDbCreator: AppDbCreator) { - this.appDbCreator = appDbCreator; - } - static getDatabaseConfig(app: Application): IDatabaseOptions { const oldConfig = app.options.database instanceof Database @@ -89,6 +84,14 @@ export class PluginMultiAppManager extends Plugin { return lodash.cloneDeep(lodash.omit(oldConfig, ['migrator'])); } + setAppOptionsFactory(factory: AppOptionsFactory) { + this.appOptionsFactory = factory; + } + + setAppDbCreator(appDbCreator: AppDbCreator) { + this.appDbCreator = appDbCreator; + } + beforeLoad() { this.db.registerModels({ ApplicationModel, @@ -96,10 +99,90 @@ export class PluginMultiAppManager extends Plugin { } async load() { - this.app.appManager.setAppSelector(async (req) => { + await this.db.import({ + directory: resolve(__dirname, 'collections'), + }); + + // after application created + this.db.on('applications.afterCreateWithAssociations', async (model: ApplicationModel, options) => { + const { transaction } = options; + + const subApp = model.registerToSupervisor(this.app, { + appOptionsFactory: this.appOptionsFactory, + }); + + // create database + await this.appDbCreator(subApp, transaction); + + const startPromise = subApp.runAsCLI(['start', '--quickstart'], { from: 'user' }); + + if (options?.context?.waitSubAppInstall) { + await startPromise; + } + }); + + this.db.on('applications.afterDestroy', async (model: ApplicationModel) => { + await AppSupervisor.getInstance().removeApp(model.get('name') as string); + }); + + const self = this; + + async function LazyLoadApplication({ + appSupervisor, + appName, + options, + }: { + appSupervisor: AppSupervisor; + appName: string; + options: any; + }) { + const name = appName; + if (appSupervisor.hasApp(name)) { + return; + } + + const applicationRecord = (await self.app.db.getRepository('applications').findOne({ + filter: { + name, + }, + })) as ApplicationModel | null; + + if (!applicationRecord) { + return; + } + + const instanceOptions = applicationRecord.get('options'); + + if (instanceOptions?.standaloneDeployment && appSupervisor.runningMode !== 'single') { + return; + } + + if (!applicationRecord) { + return; + } + + const subApp = applicationRecord.registerToSupervisor(self.app, { + appOptionsFactory: self.appOptionsFactory, + }); + + // must skip load on upgrade + if (!options?.upgrading) { + await subApp.runCommand('start'); + } + } + + AppSupervisor.getInstance().setAppBootstrapper(LazyLoadApplication); + + Gateway.getInstance().setAppSelector(async (req) => { + const appName = qs.parse(parse(req.url).query)?.__appName; + if (appName) { + return appName; + } + if (req.headers['x-app']) { return req.headers['x-app']; } + if (req.headers['x-hostname']) { const repository = this.db.getRepository('applications'); if (!repository) { @@ -110,115 +193,50 @@ export class PluginMultiAppManager extends Plugin { cname: req.headers['x-hostname'], }, }); + if (appInstance) { return appInstance.name; } } + return null; }); - await this.db.import({ - directory: resolve(__dirname, 'collections'), - }); - - // after application created - this.db.on('applications.afterCreateWithAssociations', async (model: ApplicationModel, options) => { - const { transaction } = options; - - const subApp = model.registerToMainApp(this.app, { - appOptionsFactory: this.appOptionsFactory, - }); - - // create database - await this.appDbCreator(subApp, transaction); - - // reload subApp plugin - await subApp.reload(); - - // sync subApp collections - await subApp.db.sync(); - - // install subApp - await subApp.install(); - - await subApp.reload(); - }); - - this.db.on('applications.afterDestroy', async (model: ApplicationModel) => { - await this.app.appManager.removeApplication(model.get('name') as string); - }); - - // lazy load application - // if application not in appManager, load it from database - this.app.on( - 'beforeGetApplication', - async ({ appManager, name, options }: { appManager: AppManager; name: string; options: any }) => { - await this.beforeGetApplicationMutex.runExclusive(async () => { - if (appManager.applications.has(name)) { - return; - } - - const applicationRecord = (await this.app.db.getRepository('applications').findOne({ - filter: { - name, - }, - })) as ApplicationModel | null; - - const instanceOptions = applicationRecord.get('options'); - - // skip standalone deployment application - if (instanceOptions?.standaloneDeployment && appManager.runningMode !== 'single') { - return; - } - - if (!applicationRecord) { - return; - } - - const subApp = await applicationRecord.registerToMainApp(this.app, { - appOptionsFactory: this.appOptionsFactory, - }); - - // must skip load on upgrade - if (!options?.upgrading) { - await subApp.load(); - } - }); - }, - ); - this.app.on('afterStart', async (app) => { const repository = this.db.getRepository('applications'); - const appManager = this.app.appManager; - if (appManager.runningMode == 'single') { + const appSupervisor = AppSupervisor.getInstance(); + + this.app.setMaintainingMessage('starting sub applications...'); + if (appSupervisor.runningMode == 'single') { + Gateway.getInstance().setAppSelector(() => appSupervisor.singleAppName); + // If the sub application is running in single mode, register the application automatically try { - const subApp = await repository.findOne({ - filter: { - name: appManager.singleAppName, - }, - }); - const registeredApp = await subApp.registerToMainApp(this.app, { - appOptionsFactory: this.appOptionsFactory, - }); - await registeredApp.load(); + await AppSupervisor.getInstance().getApp(appSupervisor.singleAppName); } catch (err) { - console.error('Auto register sub application in single mode failed: ', appManager.singleAppName, err); + console.error('Auto register sub application in single mode failed: ', appSupervisor.singleAppName, err); } return; } + try { const subApps = await repository.find({ filter: { 'options.autoStart': true, }, }); - for (const subApp of subApps) { - const registeredApp = await subApp.registerToMainApp(this.app, { - appOptionsFactory: this.appOptionsFactory, - }); - await registeredApp.load(); + + const promises = []; + + for (const subAppInstance of subApps) { + promises.push( + (async () => { + await AppSupervisor.getInstance().getApp(subAppInstance.name); + })(), + ); } + + await Promise.all(promises); } catch (err) { console.error('Auto register sub applications failed: ', err); } @@ -230,11 +248,11 @@ export class PluginMultiAppManager extends Plugin { const repository = this.db.getRepository('applications'); const findOptions = {}; - const appManager = this.app.appManager; + const appSupervisor = AppSupervisor.getInstance(); - if (appManager.runningMode == 'single') { + if (appSupervisor.runningMode == 'single') { findOptions['filter'] = { - name: appManager.singleAppName, + name: appSupervisor.singleAppName, }; } @@ -244,24 +262,25 @@ export class PluginMultiAppManager extends Plugin { const instanceOptions = instance.get('options'); // skip standalone deployment application - if (instanceOptions?.standaloneDeployment && appManager.runningMode !== 'single') { + if (instanceOptions?.standaloneDeployment && appSupervisor.runningMode !== 'single') { continue; } - const subApp = await appManager.getApplication(instance.name, { + const beforeSubAppStatus = AppSupervisor.getInstance().getAppStatus(instance.name); + + const subApp = await appSupervisor.getApp(instance.name, { upgrading: true, }); + console.log({ beforeSubAppStatus }); try { + this.app.setMaintainingMessage(`upgrading sub app ${instance.name}...`); console.log(`${instance.name}: upgrading...`); - await subApp.upgrade({ - cliArgs, - }); - - await subApp.stop({ - cliArgs, - }); + await subApp.runAsCLI(['upgrade'], { from: 'user' }); + if (!beforeSubAppStatus && AppSupervisor.getInstance().getAppStatus(instance.name) === 'initialized') { + await AppSupervisor.getInstance().removeApp(instance.name); + } } catch (error) { console.log(`${instance.name}: upgrade failed`); this.app.logger.error(error); @@ -293,5 +312,17 @@ export class PluginMultiAppManager extends Plugin { applications`, actions: ['applications:*'], }); + + this.app.resourcer.use(async (ctx, next) => { + await next(); + const { actionName, resourceName, params } = ctx.action; + if (actionName === 'list' && resourceName === 'applications') { + const applications = ctx.body.rows; + for (const application of applications) { + const appStatus = AppSupervisor.getInstance().getAppStatus(application.name, 'stopped'); + application.status = appStatus; + } + } + }); } } diff --git a/packages/plugins/multi-app-share-collection/src/server/__tests__/collection-sync.test.ts b/packages/plugins/multi-app-share-collection/src/server/__tests__/collection-sync.test.ts index 8846af73f..f1c556128 100644 --- a/packages/plugins/multi-app-share-collection/src/server/__tests__/collection-sync.test.ts +++ b/packages/plugins/multi-app-share-collection/src/server/__tests__/collection-sync.test.ts @@ -1,4 +1,5 @@ import { BelongsToManyRepository, Database } from '@nocobase/database'; +import { AppSupervisor } from '@nocobase/server'; import { MockServer, mockServer, pgOnly } from '@nocobase/test'; import * as process from 'process'; @@ -51,9 +52,9 @@ pgOnly()('collection sync', () => { }); await app.load(); - await app.db.sequelize.query(`DROP SCHEMA IF EXISTS sub1 CASCADE`); await app.db.sequelize.query(`DROP SCHEMA IF EXISTS sub2 CASCADE`); + await app.install({ clean: true, }); @@ -77,23 +78,25 @@ pgOnly()('collection sync', () => { values: { name: 'sub1', }, + context: { + waitSubAppInstall: true, + }, }); - const sub1 = await mainApp.appManager.getApplication('sub1'); + const sub1 = await AppSupervisor.getInstance().getApp('sub1'); + // create user at main app await mainApp.db.getRepository('users').create({ values: { email: 'test@qq.com', password: 'test123', }, }); - const defaultRole = await sub1.db.getRepository('roles').findOne({ filter: { default: true, }, }); - const user = await sub1.db.getRepository('users').findOne({ filter: { email: 'test@qq.com', @@ -107,23 +110,22 @@ pgOnly()('collection sync', () => { values: { name: 'sub2', }, + context: { + waitSubAppInstall: true, + }, }); - - const sub2 = await mainApp.appManager.getApplication('sub2'); - + const sub2 = await AppSupervisor.getInstance().getApp('sub2'); const defaultRoleInSub2 = await sub2.db.getRepository('roles').findOne({ filter: { default: true, }, }); - const userInSub2 = await sub2.db.getRepository('users').findOne({ filter: { email: 'test@qq.com', }, appends: ['roles'], }); - expect(userInSub2.get('roles').map((item) => item.name)).toContain(defaultRoleInSub2.name); }); @@ -132,32 +134,37 @@ pgOnly()('collection sync', () => { values: { name: 'sub1', }, + context: { + waitSubAppInstall: true, + }, }); - await mainApp.appManager.removeApplication('sub1'); + await AppSupervisor.getInstance().removeApp('sub1'); - const sub1 = await mainApp.appManager.getApplication('sub1'); + const sub1 = await AppSupervisor.getInstance().getApp('sub1'); expect(sub1.db.options.schema).toBe('sub1'); }); - it('should sync plugin status into lazy load sub app', async () => { + it.skip('should sync plugin status into lazy load sub app', async () => { await mainApp.db.getRepository('applications').create({ values: { name: 'sub1', }, + context: { + waitSubAppInstall: true, + }, }); - await mainApp.appManager.removeApplication('sub1'); + await AppSupervisor.getInstance().removeApp('sub1'); await mainApp.pm.enable('map'); - const sub1 = await mainApp.appManager.getApplication('sub1'); + const sub1 = await AppSupervisor.getInstance().getApp('sub1'); await sub1.reload(); - console.log(sub1.pm.plugins); - expect(sub1.pm.plugins.get('map').options.enabled).toBeTruthy(); + expect(sub1.pm.get('map').options.enabled).toBeTruthy(); const sub1MapPlugin = await sub1.db.getRepository('applicationPlugins').findOne({ filter: { @@ -173,10 +180,11 @@ pgOnly()('collection sync', () => { values: { name: 'sub1', }, + context: { + waitSubAppInstall: true, + }, }); - - const sub1 = await mainApp.appManager.getApplication('sub1'); - + const sub1 = await AppSupervisor.getInstance().getApp('sub1'); const getSubAppMapRecord = async (app) => { return await app.db.getRepository('applicationPlugins').findOne({ filter: { @@ -186,19 +194,23 @@ pgOnly()('collection sync', () => { }; expect((await getSubAppMapRecord(sub1)).get('enabled')).toBeFalsy(); - await mainApp.pm.enable('map'); - expect((await getSubAppMapRecord(sub1)).get('enabled')).toBeTruthy(); + await mainApp.pm.enable(['map']); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + expect((await getSubAppMapRecord(sub1)).get('enabled')).toBeTruthy(); // create new app sub2 await mainApp.db.getRepository('applications').create({ values: { name: 'sub2', }, + context: { + waitSubAppInstall: true, + }, }); - - const sub2 = await mainApp.appManager.getApplication('sub2'); + const sub2 = await AppSupervisor.getInstance().getApp('sub2'); expect((await getSubAppMapRecord(sub2)).get('enabled')).toBeTruthy(); - expect(sub2.pm.plugins.get('map').options.enabled).toBeTruthy(); + expect(sub2.pm.get('map').options.enabled).toBeTruthy(); }); it('should not sync roles in sub app', async () => { @@ -206,9 +218,12 @@ pgOnly()('collection sync', () => { values: { name: 'sub1', }, + context: { + waitSubAppInstall: true, + }, }); - const subApp1 = await mainApp.appManager.getApplication('sub1'); + const subApp1 = await AppSupervisor.getInstance().getApp('sub1'); await mainDb.getRepository('roles').create({ values: { @@ -230,9 +245,12 @@ pgOnly()('collection sync', () => { values: { name: 'sub1', }, + context: { + waitSubAppInstall: true, + }, }); - const subApp1 = await mainApp.appManager.getApplication(subApp1Record.name); + const subApp1 = await AppSupervisor.getInstance().getApp(subApp1Record.name); expect(subApp1.db.getCollection('users').options.schema).toBe(mainDb.options.schema || 'public'); @@ -261,9 +279,12 @@ pgOnly()('collection sync', () => { values: { name: 'sub1', }, + context: { + waitSubAppInstall: true, + }, }); - const subApp1 = await mainApp.appManager.getApplication(subApp1Record.name); + const subApp1 = await AppSupervisor.getInstance().getApp(subApp1Record.name); await mainApp.db.getRepository('collections').create({ values: { @@ -273,6 +294,8 @@ pgOnly()('collection sync', () => { context: {}, }); + await subApp1.runCommand('restart'); + const postCollection = subApp1.db.getCollection('posts'); expect(postCollection.options.schema).toBe( @@ -285,15 +308,21 @@ pgOnly()('collection sync', () => { values: { name: 'sub1', }, + context: { + waitSubAppInstall: true, + }, }); - const subApp1 = await mainApp.appManager.getApplication(subApp1Record.name); + const subApp1 = await AppSupervisor.getInstance().getApp(subApp1Record.name); const subApp2Record = await mainDb.getRepository('applications').create({ values: { name: 'sub2', }, + context: { + waitSubAppInstall: true, + }, }); - const subApp2 = await mainApp.appManager.getApplication(subApp2Record.name); + const subApp2 = await AppSupervisor.getInstance().getApp(subApp2Record.name); const mainCollection = await mainDb.getRepository('collections').create({ values: { @@ -325,9 +354,12 @@ pgOnly()('collection sync', () => { values: { name: 'sub1', }, + context: { + waitSubAppInstall: true, + }, }); - const subApp1 = await mainApp.appManager.getApplication(subApp1Record.name); + const subApp1 = await AppSupervisor.getInstance().getApp(subApp1Record.name); const mainCollection = await mainDb.getRepository('collections').create({ values: { @@ -342,6 +374,8 @@ pgOnly()('collection sync', () => { context: {}, }); + await subApp1.runCommand('restart'); + const subAppMainCollectionRecord = await subApp1.db.getRepository('collections').findOne({ filter: { name: 'mainCollection', @@ -384,6 +418,8 @@ pgOnly()('collection sync', () => { }, }); + await subApp1.runCommand('restart'); + const subRecord = await subApp1.db.getRepository('mainCollection').findOne({}); expect(subRecord.get('title')).toBe('test'); @@ -397,6 +433,8 @@ pgOnly()('collection sync', () => { }, }); + await subApp1.runCommand('restart'); + const mainCollectionTitleField2 = await subApp1.db.getRepository('fields').findOne({ filter: { collectionName: 'mainCollection', @@ -406,7 +444,7 @@ pgOnly()('collection sync', () => { expect(mainCollectionTitleField2.get('unique')).toBe(true); - expect(subAppMainCollection.getField('title')).toBeTruthy(); + expect(subApp1.db.getCollection('mainCollection').getField('title')).toBeTruthy(); await mainApp.db.getRepository('fields').destroy({ filter: { @@ -448,9 +486,12 @@ pgOnly()('collection sync', () => { values: { name: 'sub1', }, + context: { + waitSubAppInstall: true, + }, }); - const sub1 = await mainApp.appManager.getApplication('sub1'); + const sub1 = await AppSupervisor.getInstance().getApp('sub1'); await mainApp.db.getRepository('collections').create({ values: { @@ -465,6 +506,8 @@ pgOnly()('collection sync', () => { context: {}, }); + await sub1.runCommand('restart'); + const sub1CollectionsRecord = await sub1.db.getRepository('collections').findOne({ filter: { name: 'testCollection', @@ -483,6 +526,9 @@ pgOnly()('collection sync', () => { values: { name: 'sub1', }, + context: { + waitSubAppInstall: true, + }, }); await mainApp.db.getRepository('collections').create({ diff --git a/packages/plugins/multi-app-share-collection/src/server/plugin.ts b/packages/plugins/multi-app-share-collection/src/server/plugin.ts index 27a2e2071..963bda1b2 100644 --- a/packages/plugins/multi-app-share-collection/src/server/plugin.ts +++ b/packages/plugins/multi-app-share-collection/src/server/plugin.ts @@ -1,9 +1,10 @@ import PluginMultiAppManager from '@nocobase/plugin-multi-app-manager'; -import { Application, Plugin } from '@nocobase/server'; +import { Application, AppSupervisor, Plugin } from '@nocobase/server'; import lodash from 'lodash'; import { resolve } from 'path'; const subAppFilteredPlugins = ['multi-app-share-collection', 'multi-app-manager']; +const unSyncPlugins = ['localization-management']; class SubAppPlugin extends Plugin { beforeLoad() { @@ -83,6 +84,9 @@ class SubAppPlugin extends Plugin { // new subApp sync plugins from mainApp subApp.on('beforeInstall', async () => { + // sync applicationPlugins collection + await subApp.db.sync(); + const subAppPluginsCollection = subApp.db.getCollection('applicationPlugins'); const mainAppPluginsCollection = mainApp.db.getCollection('applicationPlugins'); @@ -107,6 +111,8 @@ class SubAppPlugin extends Plugin { }', (SELECT max("id") FROM ${subAppPluginsCollection.quotedTableName()})); `); + await subApp.reload(); + console.log(`sync plugins from ${mainApp.name} app to sub app ${subApp.name}`); }); } @@ -119,6 +125,10 @@ export class MultiAppShareCollectionPlugin extends Plugin { if (!this.db.inDialect('postgres')) { throw new Error('multi-app-share-collection plugin only support postgres'); } + const plugin = this.pm.get('multi-app-manager'); + if (!plugin.enabled) { + throw new Error(`${this.name} plugin need multi-app-manager plugin enabled`); + } } async beforeLoad() { @@ -135,34 +145,48 @@ export class MultiAppShareCollectionPlugin extends Plugin { if (lodash.get(options, 'loadFromDatabase')) { for (const application of await this.app.db.getCollection('applications').repository.find()) { const appName = application.get('name'); - const subApp = await this.app.appManager.getApplication(appName); + const subApp = await AppSupervisor.getInstance().getApp(appName); await callback(subApp); } return; } - const subApps = [...this.app.appManager.applications.values()]; + const subApps = [...AppSupervisor.getInstance().subApps()]; for (const subApp of subApps) { await callback(subApp); } }; - this.app.on('afterSubAppAdded', (subApp) => { - subApp.plugin(SubAppPlugin, { name: 'sub-app', mainApp: this.app }); - }); + const mainApp = this.app; + + function addPluginToSubApp(app) { + if (app.name !== 'main') { + app.plugin(SubAppPlugin, { name: 'sub-app', mainApp }); + } + } + + // if supervisor not has listen event, add listener + if ( + AppSupervisor.getInstance() + .listeners('afterAppAdded') + .filter((f) => f.name == addPluginToSubApp.name).length == 0 + ) { + AppSupervisor.getInstance().on('afterAppAdded', addPluginToSubApp); + } this.app.db.on('users.afterCreateWithAssociations', async (model, options) => { await traverseSubApps(async (subApp) => { const { transaction } = options; const repository = subApp.db.getRepository('roles'); - const subAppModel = await subApp.db.getCollection('users').repository.findOne({ + const subAppUserModel = await subApp.db.getCollection('users').repository.findOne({ filter: { id: model.get('id'), }, transaction, }); + const defaultRole = await repository.findOne({ filter: { default: true, @@ -170,80 +194,38 @@ export class MultiAppShareCollectionPlugin extends Plugin { transaction, }); - if (defaultRole && (await subAppModel.countRoles({ transaction })) == 0) { - await subAppModel.addRoles(defaultRole, { transaction }); + if (defaultRole && (await subAppUserModel.countRoles({ transaction })) == 0) { + await subAppUserModel.addRoles(defaultRole, { transaction }); } }); }); - this.app.db.on('collection:loaded', async ({ transaction, collection }) => { - await traverseSubApps(async (subApp) => { - const name = collection.name; - - const collectionRecord = await subApp.db.getRepository('collections').findOne({ - filter: { - name, - }, - transaction, - }); - - await collectionRecord.load({ transaction }); + this.app.on('__restarted', () => { + traverseSubApps((subApp) => { + subApp.runCommand('restart'); }); }); - this.app.db.on('field:loaded', async ({ transaction, fieldKey }) => { - await traverseSubApps(async (subApp) => { - const fieldRecord = await subApp.db.getRepository('fields').findOne({ - filterByTk: fieldKey, - transaction, - }); - - if (fieldRecord) { - await fieldRecord.load({ transaction }); - } - }); - }); - - this.app.on('afterEnablePlugin', async (pluginName) => { - await traverseSubApps( - async (subApp) => { + this.app.on('afterEnablePlugin', (pluginNames) => { + traverseSubApps((subApp) => { + for (const pluginName of lodash.castArray(pluginNames)) { if (subAppFilteredPlugins.includes(pluginName)) return; - await subApp.pm.enable(pluginName); - }, - { - loadFromDatabase: true, - }, - ); - }); - - this.app.on('afterDisablePlugin', async (pluginName) => { - await traverseSubApps( - async (subApp) => { - if (subAppFilteredPlugins.includes(pluginName)) return; - await subApp.pm.disable(pluginName); - }, - { - loadFromDatabase: true, - }, - ); - }); - - this.app.db.on('field.afterRemove', (removedField) => { - const subApps = [...this.app.appManager.applications.values()]; - for (const subApp of subApps) { - const collectionName = removedField.collection.name; - const collection = subApp.db.getCollection(collectionName); - if (!collection) { - subApp.log.warn(`collection ${collectionName} not found in ${subApp.name}`); - continue; + subApp.runAsCLI(['pm', 'enable', pluginName], { from: 'user' }); } + }); + }); - collection.removeField(removedField.name); - } + this.app.on('afterDisablePlugin', (pluginNames) => { + traverseSubApps((subApp) => { + for (const pluginName of lodash.castArray(pluginNames)) { + if (subAppFilteredPlugins.includes(pluginName)) return; + subApp.runAsCLI(['pm', 'disable', pluginName], { from: 'user' }); + } + }); }); this.app.db.on(`afterRemoveCollection`, (collection) => { - const subApps = [...this.app.appManager.applications.values()]; + const subApps = [...AppSupervisor.getInstance().subApps()]; for (const subApp of subApps) { subApp.db.removeCollection(collection.name); } @@ -279,7 +261,7 @@ export class MultiAppShareCollectionPlugin extends Plugin { }); // 子应用启动参数 - multiAppManager.setAppOptionsFactory((appName, mainApp) => { + multiAppManager.setAppOptionsFactory((appName, mainApp: Application) => { const mainAppDbConfig = PluginMultiAppManager.getDatabaseConfig(mainApp); const databaseOptions = { @@ -287,7 +269,7 @@ export class MultiAppShareCollectionPlugin extends Plugin { schema: appName, }; - const plugins = [...mainApp.pm.getPlugins().keys()].filter( + const plugins = [...mainApp.pm.getAliases()].filter( (name) => name !== 'multi-app-manager' && name !== 'multi-app-share-collection', ); @@ -323,10 +305,6 @@ export class MultiAppShareCollectionPlugin extends Plugin { await this.app.db.sequelize.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`); }); } - - requiredPlugins(): any[] { - return ['multi-app-manager']; - } } export default MultiAppShareCollectionPlugin; diff --git a/packages/plugins/notifications/src/server/__tests__/notifications.test.ts b/packages/plugins/notifications/src/server/__tests__/notifications.test.ts index 02a449faf..d8ef11ad2 100644 --- a/packages/plugins/notifications/src/server/__tests__/notifications.test.ts +++ b/packages/plugins/notifications/src/server/__tests__/notifications.test.ts @@ -9,8 +9,9 @@ jest.setTimeout(300000); describe('notifications', () => { let db: Database; + let app; beforeEach(async () => { - const app = mockServer(); + app = mockServer(); app.plugin(plugin); await app.load(); db = app.db; @@ -18,7 +19,7 @@ describe('notifications', () => { NotificationService.createTransport = nodemailerMock.createTransport; }); - afterEach(() => db.close()); + afterEach(() => app.destroy()); it('create', async () => { const Notification = db.getCollection('notifications'); diff --git a/packages/plugins/oidc/src/server/__tests__/oidc.test.ts b/packages/plugins/oidc/src/server/__tests__/oidc.test.ts index 134bd1f90..673ed22bf 100644 --- a/packages/plugins/oidc/src/server/__tests__/oidc.test.ts +++ b/packages/plugins/oidc/src/server/__tests__/oidc.test.ts @@ -36,7 +36,7 @@ describe('oidc', () => { }); afterAll(async () => { - await db.close(); + await app.destroy(); }); afterEach(() => { diff --git a/packages/plugins/saml/src/server/__tests__/saml.test.ts b/packages/plugins/saml/src/server/__tests__/saml.test.ts index 5ba314fa7..fd9de80bc 100644 --- a/packages/plugins/saml/src/server/__tests__/saml.test.ts +++ b/packages/plugins/saml/src/server/__tests__/saml.test.ts @@ -36,7 +36,7 @@ describe('saml', () => { }); afterAll(async () => { - await db.close(); + await app.destroy(); }); afterEach(() => { diff --git a/packages/plugins/sequence-field/src/server/__tests__/sequence-field.test.ts b/packages/plugins/sequence-field/src/server/__tests__/sequence-field.test.ts index 607421f44..96f5ed2a7 100644 --- a/packages/plugins/sequence-field/src/server/__tests__/sequence-field.test.ts +++ b/packages/plugins/sequence-field/src/server/__tests__/sequence-field.test.ts @@ -23,7 +23,7 @@ describe('sequence field', () => { }); afterEach(async () => { - await db.close(); + await app.destroy(); }); describe('define', () => { diff --git a/packages/plugins/sms-auth/src/server/__tests__/signin.test.ts b/packages/plugins/sms-auth/src/server/__tests__/signin.test.ts index dede8a30f..e5a20b301 100644 --- a/packages/plugins/sms-auth/src/server/__tests__/signin.test.ts +++ b/packages/plugins/sms-auth/src/server/__tests__/signin.test.ts @@ -49,7 +49,7 @@ describe('signin', () => { }); afterAll(async () => { - await db.close(); + await app.destroy(); }); it('should create new user and sign in via phone number', async () => { diff --git a/packages/plugins/sms-auth/src/server/plugin.ts b/packages/plugins/sms-auth/src/server/plugin.ts index 88ef9f850..b58045dca 100644 --- a/packages/plugins/sms-auth/src/server/plugin.ts +++ b/packages/plugins/sms-auth/src/server/plugin.ts @@ -1,39 +1,12 @@ -import { InstallOptions, Plugin } from '@nocobase/server'; -import { enUS, zhCN } from './locale'; -import { authType, namespace } from '../constants'; -import { SMSAuth } from './sms-auth'; import VerificationPlugin from '@nocobase/plugin-verification'; +import { InstallOptions, Plugin } from '@nocobase/server'; import { resolve } from 'path'; +import { authType } from '../constants'; +import { SMSAuth } from './sms-auth'; export class SmsAuthPlugin extends Plugin { afterAdd() {} - beforeLoad() { - this.app.i18n.addResources('zh-CN', namespace, zhCN); - this.app.i18n.addResources('en-US', namespace, enUS); - - this.app.on('afterLoad', () => { - const verificationPlugin: VerificationPlugin = this.app.getPlugin('verification'); - if (!verificationPlugin) { - this.app.logger.warn('sms-auth: @nocobase/plugin-verification is required'); - return; - } - verificationPlugin.interceptors.register('auth:signIn', { - manual: true, - getReceiver: (ctx) => { - return ctx.action.params.values.phone; - }, - expiresIn: 120, - validate: async (ctx, phone) => { - if (!phone) { - throw new Error(ctx.t('Not a valid cellphone number, please re-enter')); - } - return true; - }, - }); - }); - } - async load() { this.db.addMigrations({ namespace: 'sms-auth', @@ -43,6 +16,25 @@ export class SmsAuthPlugin extends Plugin { }, }); + const verificationPlugin: VerificationPlugin = this.app.getPlugin('verification'); + if (!verificationPlugin) { + this.app.logger.warn('sms-auth: @nocobase/plugin-verification is required'); + return; + } + verificationPlugin.interceptors.register('auth:signIn', { + manual: true, + getReceiver: (ctx) => { + return ctx.action.params.values.phone; + }, + expiresIn: 120, + validate: async (ctx, phone) => { + if (!phone) { + throw new Error(ctx.t('Not a valid cellphone number, please re-enter')); + } + return true; + }, + }); + this.app.authManager.registerTypes(authType, { auth: SMSAuth, }); diff --git a/packages/plugins/ui-schema-storage/src/server/__tests__/action.test.ts b/packages/plugins/ui-schema-storage/src/server/__tests__/action.test.ts index 7c0ab870a..4908e8f37 100644 --- a/packages/plugins/ui-schema-storage/src/server/__tests__/action.test.ts +++ b/packages/plugins/ui-schema-storage/src/server/__tests__/action.test.ts @@ -24,6 +24,8 @@ describe('action test', () => { drop: false, }, }); + + await app.start(); }); afterEach(async () => { diff --git a/packages/plugins/users/src/server/__tests__/actions.test.ts b/packages/plugins/users/src/server/__tests__/actions.test.ts index 354f9e27d..1a1329f1b 100644 --- a/packages/plugins/users/src/server/__tests__/actions.test.ts +++ b/packages/plugins/users/src/server/__tests__/actions.test.ts @@ -35,7 +35,7 @@ describe('actions', () => { }); afterEach(async () => { - await db.close(); + await app.destroy(); }); // it('should login user with password', async () => { diff --git a/packages/plugins/users/src/server/__tests__/fields.test.ts b/packages/plugins/users/src/server/__tests__/fields.test.ts index 0efdc5762..391c27408 100644 --- a/packages/plugins/users/src/server/__tests__/fields.test.ts +++ b/packages/plugins/users/src/server/__tests__/fields.test.ts @@ -16,7 +16,7 @@ describe('createdBy/updatedBy', () => { }); afterEach(async () => { - await db.close(); + await api.destroy(); }); describe('collection definition', () => { diff --git a/packages/plugins/workflow/src/server/Plugin.ts b/packages/plugins/workflow/src/server/Plugin.ts index 21c7246f9..17ede22db 100644 --- a/packages/plugins/workflow/src/server/Plugin.ts +++ b/packages/plugins/workflow/src/server/Plugin.ts @@ -1,22 +1,22 @@ import path from 'path'; -import winston from 'winston'; import LRUCache from 'lru-cache'; +import winston from 'winston'; import { Op } from '@nocobase/database'; import { Plugin } from '@nocobase/server'; import { Registry } from '@nocobase/utils'; -import initFields from './fields'; +import { createLogger, getLoggerFilePath, getLoggerLevel, Logger, LoggerOptions } from '@nocobase/logger'; +import Processor from './Processor'; import initActions from './actions'; import { EXECUTION_STATUS } from './constants'; -import initInstructions, { Instruction } from './instructions'; -import Processor from './Processor'; -import initTriggers, { Trigger } from './triggers'; +import initFields from './fields'; import initFunctions, { CustomFunction } from './functions'; -import { createLogger, Logger, LoggerOptions, getLoggerLevel, getLoggerFilePath } from '@nocobase/logger'; +import initInstructions, { Instruction } from './instructions'; +import initTriggers, { Trigger } from './triggers'; -import type { WorkflowModel, ExecutionModel, JobModel } from './types'; +import type { ExecutionModel, JobModel, WorkflowModel } from './types'; type Pending = [ExecutionModel, JobModel?]; @@ -169,6 +169,7 @@ export default class WorkflowPlugin extends Plugin { }); this.app.on('afterStart', () => { + this.app.setMaintainingMessage('check for not started executions'); // check for not started executions this.dispatch(); }); @@ -221,6 +222,19 @@ export default class WorkflowPlugin extends Plugin { setTimeout(this.prepare); } + public async resume(job) { + if (!job.execution) { + job.execution = await job.getExecution(); + } + + this.pending.push([job.execution, job]); + this.dispatch(); + } + + public createProcessor(execution: ExecutionModel, options = {}): Processor { + return new Processor(execution, { ...options, plugin: this }); + } + private prepare = async () => { const [event] = this.events; if (!event) { @@ -290,15 +304,6 @@ export default class WorkflowPlugin extends Plugin { } }; - public async resume(job) { - if (!job.execution) { - job.execution = await job.getExecution(); - } - - this.pending.push([job.execution, job]); - this.dispatch(); - } - private async dispatch() { if (this.executing) { return; @@ -356,8 +361,4 @@ export default class WorkflowPlugin extends Plugin { this.getLogger(execution.workflowId).error(`execution (${execution.id}) error: ${err.message}`, err); } } - - public createProcessor(execution: ExecutionModel, options = {}): Processor { - return new Processor(execution, { ...options, plugin: this }); - } } diff --git a/packages/plugins/workflow/src/server/__tests__/instructions/delay.test.ts b/packages/plugins/workflow/src/server/__tests__/instructions/delay.test.ts index 96ba06242..5fb3e4cd1 100644 --- a/packages/plugins/workflow/src/server/__tests__/instructions/delay.test.ts +++ b/packages/plugins/workflow/src/server/__tests__/instructions/delay.test.ts @@ -28,7 +28,7 @@ describe('workflow > instructions > delay', () => { }); }); - afterEach(() => app.stop()); + afterEach(() => app.destroy()); describe('runtime', () => { it('delay to resolved', async () => { diff --git a/packages/plugins/workflow/src/server/__tests__/instructions/loop.test.ts b/packages/plugins/workflow/src/server/__tests__/instructions/loop.test.ts index d87bb101d..e89be2038 100644 --- a/packages/plugins/workflow/src/server/__tests__/instructions/loop.test.ts +++ b/packages/plugins/workflow/src/server/__tests__/instructions/loop.test.ts @@ -29,7 +29,7 @@ describe('workflow > instructions > loop', () => { }); }); - afterEach(() => app.stop()); + afterEach(() => app.destroy()); describe('branch', () => { it('no branch just pass', async () => { diff --git a/packages/plugins/workflow/src/server/__tests__/instructions/parallel.test.ts b/packages/plugins/workflow/src/server/__tests__/instructions/parallel.test.ts index 3075825f7..a9fa10d4e 100644 --- a/packages/plugins/workflow/src/server/__tests__/instructions/parallel.test.ts +++ b/packages/plugins/workflow/src/server/__tests__/instructions/parallel.test.ts @@ -29,7 +29,7 @@ describe('workflow > instructions > parallel', () => { }); }); - afterEach(() => app.stop()); + afterEach(() => app.destroy()); describe('single all', () => { it('all resolved', async () => { diff --git a/packages/plugins/workflow/src/server/__tests__/instructions/request.test.ts b/packages/plugins/workflow/src/server/__tests__/instructions/request.test.ts index 96bcb9001..116daa926 100644 --- a/packages/plugins/workflow/src/server/__tests__/instructions/request.test.ts +++ b/packages/plugins/workflow/src/server/__tests__/instructions/request.test.ts @@ -1,4 +1,4 @@ -import { Application } from '@nocobase/server'; +import { Application, Gateway } from '@nocobase/server'; import Database from '@nocobase/database'; import { getApp, sleep } from '..'; import { RequestConfig } from '../../instructions/request'; @@ -6,9 +6,9 @@ import { EXECUTION_STATUS, JOB_STATUS } from '../../constants'; const PORT = 12345; -const URL_DATA = `http://localhost:${PORT}/data`; +const URL_DATA = `http://localhost:${PORT}/api/data`; const URL_400 = `http://localhost:${PORT}/api/400`; -const URL_TIMEOUT = `http://localhost:${PORT}/timeout`; +const URL_TIMEOUT = `http://localhost:${PORT}/api/timeout`; describe('workflow > instructions > request', () => { let app: Application; @@ -24,11 +24,11 @@ describe('workflow > instructions > request', () => { if (ctx.path === '/api/400') { return ctx.throw(400); } - if (ctx.path === '/timeout') { + if (ctx.path === '/api/timeout') { await sleep(2000); return ctx.throw(new Error('timeout')); } - if (ctx.path === '/data') { + if (ctx.path === '/api/data') { ctx.withoutDataWrapping = true; ctx.body = { meta: { title: ctx.query.title }, @@ -38,7 +38,12 @@ describe('workflow > instructions > request', () => { next(); }); - await app.start({ listen: { port: PORT } }); + Gateway.getInstance().start({ + port: PORT, + host: 'localhost', + }); + + await app.start(); db = app.db; WorkflowModel = db.getCollection('workflows').model; @@ -55,7 +60,7 @@ describe('workflow > instructions > request', () => { }); }); - afterEach(() => app.stop()); + afterEach(() => app.destroy()); describe('request', () => { it('request', async () => { @@ -214,7 +219,7 @@ describe('workflow > instructions > request', () => { expect(job.result.data).toEqual({ title }); }); - it('request inside loop',async () => { + it('request inside loop', async () => { const n1 = await workflow.createNode({ type: 'loop', config: { @@ -229,7 +234,7 @@ describe('workflow > instructions > request', () => { config: { url: URL_DATA, method: 'GET', - } + }, }); await PostRepo.create({ values: { title: 't1' } }); @@ -240,7 +245,7 @@ describe('workflow > instructions > request', () => { expect(execution.status).toEqual(EXECUTION_STATUS.RESOLVED); const jobs = await execution.getJobs({ order: [['id', 'ASC']] }); expect(jobs.length).toBe(3); - expect(jobs.map(item => item.status)).toEqual(Array(3).fill(JOB_STATUS.RESOLVED)); + expect(jobs.map((item) => item.status)).toEqual(Array(3).fill(JOB_STATUS.RESOLVED)); expect(jobs[0].result).toBe(2); }); }); diff --git a/packages/plugins/workflow/src/server/__tests__/triggers/form.test.ts b/packages/plugins/workflow/src/server/__tests__/triggers/form.test.ts index 6dab59493..eeb32cb24 100644 --- a/packages/plugins/workflow/src/server/__tests__/triggers/form.test.ts +++ b/packages/plugins/workflow/src/server/__tests__/triggers/form.test.ts @@ -34,7 +34,7 @@ describe('workflow > triggers > form', () => { userAgents = users.map((user) => app.agent().login(user)); }); - afterEach(() => app.stop()); + afterEach(() => app.destroy()); describe('create', () => { it('enabled / disabled', async () => { diff --git a/packages/presets/nocobase/src/server/index.ts b/packages/presets/nocobase/src/server/index.ts index 470585653..4cbe3735d 100644 --- a/packages/presets/nocobase/src/server/index.ts +++ b/packages/presets/nocobase/src/server/index.ts @@ -61,98 +61,6 @@ export class PresetNocoBase extends Plugin { return _.uniq(this.splitNames(APPEND_PRESET_LOCAL_PLUGINS).concat(this.localPlugins)); } - async addBuiltInPlugins(options?: any) { - const builtInPlugins = this.getBuiltInPlugins(); - - await this.app.pm.add(builtInPlugins, { - enabled: true, - builtIn: true, - installed: true, - }); - - const localPlugins = this.getLocalPlugins(); - await this.app.pm.add(localPlugins, {}); - await this.app.reload({ method: options.method }); - } - - afterAdd() { - this.app.on('beforeLoad', async (app, options) => { - if (options?.method !== 'upgrade') { - return; - } - const version = await this.app.version.get(); - console.log(`The version number before upgrade is ${version}`); - }); - - this.app.on('beforeUpgrade', async () => { - if (!this.db.inDialect('sqlite')) { - return; - } - await this.app.load({ method: 'upgrade' }); - const Field = this.db.getRepository('fields'); - const existed = await Field.count({ - filter: { - name: 'username', - collectionName: 'users', - }, - }); - if (!existed) { - await this.db.getRepository('fields').create({ - values: { - name: 'username', - collectionName: 'users', - type: 'string', - unique: true, - interface: 'input', - uiSchema: { - type: 'string', - title: '{{t("Username")}}', - 'x-component': 'Input', - 'x-validator': { username: true }, - required: true, - }, - }, - // NOTE: to trigger hook - context: {}, - }); - } - }); - - this.app.on('beforeUpgrade', async () => { - const result = await this.app.version.satisfies('<0.8.0-alpha.1'); - - if (result) { - console.log(`Initialize all built-in plugins beforeUpgrade`); - await this.addBuiltInPlugins({ method: 'upgrade' }); - } - - const builtInPlugins = this.getBuiltInPlugins(); - const plugins = await this.app.db.getRepository('applicationPlugins').find(); - const pluginNames = plugins.map((p) => p.name); - await this.app.pm.add( - builtInPlugins.filter((plugin) => !pluginNames.includes(plugin)), - { - enabled: true, - builtIn: true, - installed: true, - }, - ); - const localPlugins = this.getLocalPlugins(); - await this.app.pm.add( - localPlugins.filter((plugin) => !pluginNames.includes(plugin)), - {}, - ); - await this.app.reload({ method: 'upgrade' }); - await this.app.db.sync(); - await this.app.db.getRepository('collections').load(); - }); - - this.app.on('beforeInstall', async () => { - console.log(`Initialize all built-in plugins beforeInstall in ${this.app.name}`); - await this.addBuiltInPlugins({ method: 'install' }); - }); - } - beforeLoad() { this.db.addMigrations({ namespace: this.getName(), @@ -162,6 +70,26 @@ export class PresetNocoBase extends Plugin { }, }); } + + get allPlugins() { + return this.builtInPlugins + .map((name) => { + return { name, enabled: true, builtIn: true } as any; + }) + .concat(this.localPlugins.map((name) => ({ name }))); + } + + async install() { + const repository = this.app.db.getRepository('applicationPlugins'); + const existPlugins = await repository.find(); + const existPluginNames = existPlugins.map((item) => item.name); + const plugins = this.allPlugins.filter((item) => !existPluginNames.includes(item.name)); + await repository.create({ values: plugins }); + this.log.debug('install preset plugins'); + await repository.init(); + await this.app.pm.load(); + await this.app.pm.install(); + } } export default PresetNocoBase; diff --git a/packages/samples/custom-collection-template/src/client/index.tsx b/packages/samples/custom-collection-template/src/client/index.tsx index a08cb0eec..c97557943 100644 --- a/packages/samples/custom-collection-template/src/client/index.tsx +++ b/packages/samples/custom-collection-template/src/client/index.tsx @@ -44,10 +44,10 @@ const myCollectionTemplate: ICollectionTemplate = { }, }; -registerTemplate('myCollection', myCollectionTemplate); - class CustomCollectionPlugin extends Plugin { - async load() { } + async load() { + registerTemplate('myCollection', myCollectionTemplate); + } } export default CustomCollectionPlugin; diff --git a/yarn.lock b/yarn.lock index b9f60a107..afa58753b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -594,6 +594,53 @@ dependencies: tslib "^2.5.0" +"@aws-sdk/client-lambda@^3.363.0": + version "3.395.0" + resolved "https://registry.npmmirror.com/@aws-sdk/client-lambda/-/client-lambda-3.395.0.tgz#e274d5893d0bd3bd74f7fb44945304e058427e19" + integrity sha512-zweX9axjFPR1RwE3bRsBa/1cKYGzhrPsPo370DrQUZdz6TjEt1n/DRYQl0Bo0P1Z6ifjNx85YLIxDs0iijkLiA== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.395.0" + "@aws-sdk/credential-provider-node" "3.395.0" + "@aws-sdk/middleware-host-header" "3.391.0" + "@aws-sdk/middleware-logger" "3.391.0" + "@aws-sdk/middleware-recursion-detection" "3.391.0" + "@aws-sdk/middleware-signing" "3.391.0" + "@aws-sdk/middleware-user-agent" "3.391.0" + "@aws-sdk/types" "3.391.0" + "@aws-sdk/util-endpoints" "3.391.0" + "@aws-sdk/util-user-agent-browser" "3.391.0" + "@aws-sdk/util-user-agent-node" "3.391.0" + "@smithy/config-resolver" "^2.0.3" + "@smithy/eventstream-serde-browser" "^2.0.3" + "@smithy/eventstream-serde-config-resolver" "^2.0.3" + "@smithy/eventstream-serde-node" "^2.0.3" + "@smithy/fetch-http-handler" "^2.0.3" + "@smithy/hash-node" "^2.0.3" + "@smithy/invalid-dependency" "^2.0.3" + "@smithy/middleware-content-length" "^2.0.3" + "@smithy/middleware-endpoint" "^2.0.3" + "@smithy/middleware-retry" "^2.0.3" + "@smithy/middleware-serde" "^2.0.3" + "@smithy/middleware-stack" "^2.0.0" + "@smithy/node-config-provider" "^2.0.3" + "@smithy/node-http-handler" "^2.0.3" + "@smithy/protocol-http" "^2.0.3" + "@smithy/smithy-client" "^2.0.3" + "@smithy/types" "^2.2.0" + "@smithy/url-parser" "^2.0.3" + "@smithy/util-base64" "^2.0.0" + "@smithy/util-body-length-browser" "^2.0.0" + "@smithy/util-body-length-node" "^2.0.0" + "@smithy/util-defaults-mode-browser" "^2.0.3" + "@smithy/util-defaults-mode-node" "^2.0.3" + "@smithy/util-retry" "^2.0.0" + "@smithy/util-stream" "^2.0.3" + "@smithy/util-utf8" "^2.0.0" + "@smithy/util-waiter" "^2.0.3" + tslib "^2.5.0" + "@aws-sdk/client-s3@^3.245.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/client-s3/-/client-s3-3.363.0.tgz#757384d92c32a407ab0aaecf6f54d47cd9053514" @@ -729,6 +776,45 @@ "@smithy/util-utf8" "^1.0.1" tslib "^2.5.0" +"@aws-sdk/client-sso@3.395.0": + version "3.395.0" + resolved "https://registry.npmmirror.com/@aws-sdk/client-sso/-/client-sso-3.395.0.tgz#9604d3e6c131a48bd3e2421cb0eefc36faa50fc6" + integrity sha512-IEmqpZnflzFk6NTlkRpEXIcU2uBrTYl+pA5z4ZerbKclYWuxJ7MoLtLDNWgIn3mkNxvdroWgaPY1B2dkQlTe4g== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/middleware-host-header" "3.391.0" + "@aws-sdk/middleware-logger" "3.391.0" + "@aws-sdk/middleware-recursion-detection" "3.391.0" + "@aws-sdk/middleware-user-agent" "3.391.0" + "@aws-sdk/types" "3.391.0" + "@aws-sdk/util-endpoints" "3.391.0" + "@aws-sdk/util-user-agent-browser" "3.391.0" + "@aws-sdk/util-user-agent-node" "3.391.0" + "@smithy/config-resolver" "^2.0.3" + "@smithy/fetch-http-handler" "^2.0.3" + "@smithy/hash-node" "^2.0.3" + "@smithy/invalid-dependency" "^2.0.3" + "@smithy/middleware-content-length" "^2.0.3" + "@smithy/middleware-endpoint" "^2.0.3" + "@smithy/middleware-retry" "^2.0.3" + "@smithy/middleware-serde" "^2.0.3" + "@smithy/middleware-stack" "^2.0.0" + "@smithy/node-config-provider" "^2.0.3" + "@smithy/node-http-handler" "^2.0.3" + "@smithy/protocol-http" "^2.0.3" + "@smithy/smithy-client" "^2.0.3" + "@smithy/types" "^2.2.0" + "@smithy/url-parser" "^2.0.3" + "@smithy/util-base64" "^2.0.0" + "@smithy/util-body-length-browser" "^2.0.0" + "@smithy/util-body-length-node" "^2.0.0" + "@smithy/util-defaults-mode-browser" "^2.0.3" + "@smithy/util-defaults-mode-node" "^2.0.3" + "@smithy/util-retry" "^2.0.0" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.5.0" + "@aws-sdk/client-sts@3.363.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/client-sts/-/client-sts-3.363.0.tgz#c02b3cf3bd2ef9d54195323370db964cd1df4711" @@ -771,6 +857,49 @@ fast-xml-parser "4.2.5" tslib "^2.5.0" +"@aws-sdk/client-sts@3.395.0": + version "3.395.0" + resolved "https://registry.npmmirror.com/@aws-sdk/client-sts/-/client-sts-3.395.0.tgz#b70374b8ce171e2251fd8bbba71c8c1a13eed1c8" + integrity sha512-zWxZ+pjeP88uRN4k0Zzid6t/8Yhzg1Cv2LnrYX6kZzbS6AOTDho7fVGZgUl+cme33QZhtE8pXUvwGeJAptbhqg== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/credential-provider-node" "3.395.0" + "@aws-sdk/middleware-host-header" "3.391.0" + "@aws-sdk/middleware-logger" "3.391.0" + "@aws-sdk/middleware-recursion-detection" "3.391.0" + "@aws-sdk/middleware-sdk-sts" "3.391.0" + "@aws-sdk/middleware-signing" "3.391.0" + "@aws-sdk/middleware-user-agent" "3.391.0" + "@aws-sdk/types" "3.391.0" + "@aws-sdk/util-endpoints" "3.391.0" + "@aws-sdk/util-user-agent-browser" "3.391.0" + "@aws-sdk/util-user-agent-node" "3.391.0" + "@smithy/config-resolver" "^2.0.3" + "@smithy/fetch-http-handler" "^2.0.3" + "@smithy/hash-node" "^2.0.3" + "@smithy/invalid-dependency" "^2.0.3" + "@smithy/middleware-content-length" "^2.0.3" + "@smithy/middleware-endpoint" "^2.0.3" + "@smithy/middleware-retry" "^2.0.3" + "@smithy/middleware-serde" "^2.0.3" + "@smithy/middleware-stack" "^2.0.0" + "@smithy/node-config-provider" "^2.0.3" + "@smithy/node-http-handler" "^2.0.3" + "@smithy/protocol-http" "^2.0.3" + "@smithy/smithy-client" "^2.0.3" + "@smithy/types" "^2.2.0" + "@smithy/url-parser" "^2.0.3" + "@smithy/util-base64" "^2.0.0" + "@smithy/util-body-length-browser" "^2.0.0" + "@smithy/util-body-length-node" "^2.0.0" + "@smithy/util-defaults-mode-browser" "^2.0.3" + "@smithy/util-defaults-mode-node" "^2.0.3" + "@smithy/util-retry" "^2.0.0" + "@smithy/util-utf8" "^2.0.0" + fast-xml-parser "4.2.5" + tslib "^2.5.0" + "@aws-sdk/credential-provider-env@3.363.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.363.0.tgz#5b8471a243cdb54696ecae99ad4cc1c48d687657" @@ -780,6 +909,16 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@aws-sdk/credential-provider-env@3.391.0": + version "3.391.0" + resolved "https://registry.npmmirror.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.391.0.tgz#95ee11d77572809f4d88b3e219b9685625612d66" + integrity sha512-mAzICedcg4bfL0mM5O6QTd9mQ331NLse1DMr6XL21ZZiLB48ej19L7AGV2xq5QwVbqKU3IVv1myRyhvpDM9jMg== + dependencies: + "@aws-sdk/types" "3.391.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/types" "^2.2.0" + tslib "^2.5.0" + "@aws-sdk/credential-provider-ini@3.363.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.363.0.tgz#e77e65e1ffc7c736aa724ebdf038e99dca57a87b" @@ -795,6 +934,22 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@aws-sdk/credential-provider-ini@3.395.0": + version "3.395.0" + resolved "https://registry.npmmirror.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.395.0.tgz#d8d85300bcf3888a09c95700a5d5a24e10a44def" + integrity sha512-t7cWs+syJsSkj9NGdKyZ1t/+nYQyOec2nPjTtPWwKs8D7rvH3IMIgJwkvAGNzYaiIoIpXXx0wgCqys84TSEIYQ== + dependencies: + "@aws-sdk/credential-provider-env" "3.391.0" + "@aws-sdk/credential-provider-process" "3.391.0" + "@aws-sdk/credential-provider-sso" "3.395.0" + "@aws-sdk/credential-provider-web-identity" "3.391.0" + "@aws-sdk/types" "3.391.0" + "@smithy/credential-provider-imds" "^2.0.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.0" + "@smithy/types" "^2.2.0" + tslib "^2.5.0" + "@aws-sdk/credential-provider-node@3.363.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.363.0.tgz#70815b3c8bc98d9afd148b851c8fdae9ce11fcd6" @@ -811,6 +966,23 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@aws-sdk/credential-provider-node@3.395.0": + version "3.395.0" + resolved "https://registry.npmmirror.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.395.0.tgz#79ebf5c9c9245a07ef8d9cf88b3083f35c239628" + integrity sha512-qJawWTYf5L7Z1Is0sSJEYc4e96Qd0HWGqluO2h9qoUNrRREZ9RSxsDq+LGxVVAYLupYFcIFtiCnA/MoBBIWhzg== + dependencies: + "@aws-sdk/credential-provider-env" "3.391.0" + "@aws-sdk/credential-provider-ini" "3.395.0" + "@aws-sdk/credential-provider-process" "3.391.0" + "@aws-sdk/credential-provider-sso" "3.395.0" + "@aws-sdk/credential-provider-web-identity" "3.391.0" + "@aws-sdk/types" "3.391.0" + "@smithy/credential-provider-imds" "^2.0.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.0" + "@smithy/types" "^2.2.0" + tslib "^2.5.0" + "@aws-sdk/credential-provider-process@3.363.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.363.0.tgz#08608f6da246084f9b20481ac0de17f04ae54b4d" @@ -821,6 +993,17 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@aws-sdk/credential-provider-process@3.391.0": + version "3.391.0" + resolved "https://registry.npmmirror.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.391.0.tgz#7f008fa719680dfeab35d77fa6787b7b31b62143" + integrity sha512-KMlzPlBI+hBmXDo+EoFZdLgCVRkRa9B9iEE6x0+hQQ6g9bW6HI7cDRVdceR1ZoPasSaNAZ9QOXMTIBxTpn0sPQ== + dependencies: + "@aws-sdk/types" "3.391.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.0" + "@smithy/types" "^2.2.0" + tslib "^2.5.0" + "@aws-sdk/credential-provider-sso@3.363.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.363.0.tgz#949190c9ea510d9772aef9c61345575f4b40b44d" @@ -833,6 +1016,19 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@aws-sdk/credential-provider-sso@3.395.0": + version "3.395.0" + resolved "https://registry.npmmirror.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.395.0.tgz#b3362c6afd72b52ce9438c1715f3ed90358cd355" + integrity sha512-wAoHG9XqO0L8TvJv4cjwN/2XkYskp0cbnupKKTJm+D29MYcctKEtL0aYOHxaNN2ECAYxIFIQDdlo62GKb3nJ5Q== + dependencies: + "@aws-sdk/client-sso" "3.395.0" + "@aws-sdk/token-providers" "3.391.0" + "@aws-sdk/types" "3.391.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/shared-ini-file-loader" "^2.0.0" + "@smithy/types" "^2.2.0" + tslib "^2.5.0" + "@aws-sdk/credential-provider-web-identity@3.363.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.363.0.tgz#a5312519126ff7c3fea56ffefa0e51ef9383663c" @@ -842,6 +1038,16 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@aws-sdk/credential-provider-web-identity@3.391.0": + version "3.391.0" + resolved "https://registry.npmmirror.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.391.0.tgz#c27aa6f2a215601a444ad7e3259f3ed55ccb39e7" + integrity sha512-n0vYg82B8bc4rxKltVbVqclev7hx+elyS9pEnZs3YbnbWJq0qqsznXmDfLqd1TcWpa09PGXcah0nsRDolVThsA== + dependencies: + "@aws-sdk/types" "3.391.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/types" "^2.2.0" + tslib "^2.5.0" + "@aws-sdk/hash-blob-browser@3.357.0": version "3.357.0" resolved "https://registry.npmmirror.com/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.357.0.tgz#e507929499fe0fe128664b67cd26f63f16ed4d25" @@ -925,6 +1131,16 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@aws-sdk/middleware-host-header@3.391.0": + version "3.391.0" + resolved "https://registry.npmmirror.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.391.0.tgz#80e9745880b671562ff115cd189ea929da51acc3" + integrity sha512-+nyNr0rb2ixY7mU48nibr7L7gsw37y4oELhqgnNKhcjZDJ34imBwKIMFa64n21FdftmhcjR8IdSpzXE9xrkJ8g== + dependencies: + "@aws-sdk/types" "3.391.0" + "@smithy/protocol-http" "^2.0.3" + "@smithy/types" "^2.2.0" + tslib "^2.5.0" + "@aws-sdk/middleware-location-constraint@3.363.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.363.0.tgz#48c8a16698d7678578a5f06e0eb7f8118ec86f82" @@ -941,6 +1157,15 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@aws-sdk/middleware-logger@3.391.0": + version "3.391.0" + resolved "https://registry.npmmirror.com/@aws-sdk/middleware-logger/-/middleware-logger-3.391.0.tgz#b0c61b3599dc9efddb6182337eb6362e3712dadc" + integrity sha512-KOwl5zo16b17JDhqILHBStccBQ2w35em7+/6vdkJdUII6OU8aVIFTlIQT9wOUvd4do6biIRBMZG3IK0Rg7mRDQ== + dependencies: + "@aws-sdk/types" "3.391.0" + "@smithy/types" "^2.2.0" + tslib "^2.5.0" + "@aws-sdk/middleware-recursion-detection@3.363.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.363.0.tgz#bd8b8010f5be5d7e90a97bf9e55a7980289b1600" @@ -950,6 +1175,16 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@aws-sdk/middleware-recursion-detection@3.391.0": + version "3.391.0" + resolved "https://registry.npmmirror.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.391.0.tgz#010334cd7945b4b6712f33e2bf0f54d69f214e7b" + integrity sha512-hVR3z59G7pX4pjDQs9Ag1tMgbLeGXOzeAAaNP9fEtHSd3KBMAGQgN3K3b9WPjzE2W0EoloHRJMK4qxZErdde2g== + dependencies: + "@aws-sdk/types" "3.391.0" + "@smithy/protocol-http" "^2.0.3" + "@smithy/types" "^2.2.0" + tslib "^2.5.0" + "@aws-sdk/middleware-sdk-s3@3.363.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.363.0.tgz#35e77ad14b6799b0be1c313d9c8b7ca0ad2f4fdb" @@ -969,6 +1204,16 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@aws-sdk/middleware-sdk-sts@3.391.0": + version "3.391.0" + resolved "https://registry.npmmirror.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.391.0.tgz#0e0254ab4c59577c8646ab67939039d977ba93c0" + integrity sha512-6ZXI3Z4QU+TnT5PwKWloGmRHG81tWeI18/zxf9wWzrO2NhYFvITzEJH0vWLLiXdWtn/BYfLULXtDvkTaepbI5A== + dependencies: + "@aws-sdk/middleware-signing" "3.391.0" + "@aws-sdk/types" "3.391.0" + "@smithy/types" "^2.2.0" + tslib "^2.5.0" + "@aws-sdk/middleware-signing@3.363.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/middleware-signing/-/middleware-signing-3.363.0.tgz#81067698e0566584f0ca30be56232758f69e2232" @@ -981,6 +1226,19 @@ "@smithy/util-middleware" "^1.0.1" tslib "^2.5.0" +"@aws-sdk/middleware-signing@3.391.0": + version "3.391.0" + resolved "https://registry.npmmirror.com/@aws-sdk/middleware-signing/-/middleware-signing-3.391.0.tgz#f16ca8a9a3fa750f4f0f6a4b1baeb4899bf675f6" + integrity sha512-2pAJJlZqaHc0d+cz2FTVrQmWi8ygKfqfczHUo/loCtOaMNtWXBHb/JsLEecs6cXdizy6gi3YsLz6VZYwY4Ssxw== + dependencies: + "@aws-sdk/types" "3.391.0" + "@smithy/property-provider" "^2.0.0" + "@smithy/protocol-http" "^2.0.3" + "@smithy/signature-v4" "^2.0.0" + "@smithy/types" "^2.2.0" + "@smithy/util-middleware" "^2.0.0" + tslib "^2.5.0" + "@aws-sdk/middleware-ssec@3.363.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.363.0.tgz#9dde9e09660bcb6a0d39939d7f1ab043b93fefdb" @@ -999,6 +1257,17 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@aws-sdk/middleware-user-agent@3.391.0": + version "3.391.0" + resolved "https://registry.npmmirror.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.391.0.tgz#bcbafbefc1e04966acab4f19662c8a4cea90e7a4" + integrity sha512-LdK9uMNA14zqRw3B79Mhy7GX36qld/GYo93xuu+lr+AQ98leZEdc6GUbrtNDI3fP1Z8TMQcyHUKBml4/B+wXpQ== + dependencies: + "@aws-sdk/types" "3.391.0" + "@aws-sdk/util-endpoints" "3.391.0" + "@smithy/protocol-http" "^2.0.3" + "@smithy/types" "^2.2.0" + tslib "^2.5.0" + "@aws-sdk/signature-v4-multi-region@3.363.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.363.0.tgz#7c64ba8a6af6f52f73ef849d4fcdd102f63ad606" @@ -1020,12 +1289,61 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@aws-sdk/token-providers@3.391.0": + version "3.391.0" + resolved "https://registry.npmmirror.com/@aws-sdk/token-providers/-/token-providers-3.391.0.tgz#a6706d88e3a5d603c263a4d505fd1186e9cee171" + integrity sha512-kgfArsKLDJE71qQjfXiHiM5cZqgDHlMsqEx35+A65GmTWJaS1PGDqu3ZvVVU8E5mxnCCLw7vho21fsjvH6TBpg== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/middleware-host-header" "3.391.0" + "@aws-sdk/middleware-logger" "3.391.0" + "@aws-sdk/middleware-recursion-detection" "3.391.0" + "@aws-sdk/middleware-user-agent" "3.391.0" + "@aws-sdk/types" "3.391.0" + "@aws-sdk/util-endpoints" "3.391.0" + "@aws-sdk/util-user-agent-browser" "3.391.0" + "@aws-sdk/util-user-agent-node" "3.391.0" + "@smithy/config-resolver" "^2.0.3" + "@smithy/fetch-http-handler" "^2.0.3" + "@smithy/hash-node" "^2.0.3" + "@smithy/invalid-dependency" "^2.0.3" + "@smithy/middleware-content-length" "^2.0.3" + "@smithy/middleware-endpoint" "^2.0.3" + "@smithy/middleware-retry" "^2.0.3" + "@smithy/middleware-serde" "^2.0.3" + "@smithy/middleware-stack" "^2.0.0" + "@smithy/node-config-provider" "^2.0.3" + "@smithy/node-http-handler" "^2.0.3" + "@smithy/property-provider" "^2.0.0" + "@smithy/protocol-http" "^2.0.3" + "@smithy/shared-ini-file-loader" "^2.0.0" + "@smithy/smithy-client" "^2.0.3" + "@smithy/types" "^2.2.0" + "@smithy/url-parser" "^2.0.3" + "@smithy/util-base64" "^2.0.0" + "@smithy/util-body-length-browser" "^2.0.0" + "@smithy/util-body-length-node" "^2.0.0" + "@smithy/util-defaults-mode-browser" "^2.0.3" + "@smithy/util-defaults-mode-node" "^2.0.3" + "@smithy/util-retry" "^2.0.0" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.5.0" + "@aws-sdk/types@3.357.0", "@aws-sdk/types@^3.222.0": version "3.357.0" resolved "https://registry.npmmirror.com/@aws-sdk/types/-/types-3.357.0.tgz#8491da71a4291cc2661c26a75089e86532b6a3b5" dependencies: tslib "^2.5.0" +"@aws-sdk/types@3.391.0": + version "3.391.0" + resolved "https://registry.npmmirror.com/@aws-sdk/types/-/types-3.391.0.tgz#d49b0130943f0c60fd9bc99b2a47ec9720e2dd07" + integrity sha512-QpYVFKMOnzHz/JMj/b8wb18qxiT92U/5r5MmtRz2R3LOH6ooTO96k4ozXCrYr0qNed1PAnOj73rPrrH2wnCJKQ== + dependencies: + "@smithy/types" "^2.2.0" + tslib "^2.5.0" + "@aws-sdk/util-arn-parser@3.310.0": version "3.310.0" resolved "https://registry.npmmirror.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.310.0.tgz#861ff8810851be52a320ec9e4786f15b5fc74fba" @@ -1046,6 +1364,14 @@ "@aws-sdk/types" "3.357.0" tslib "^2.5.0" +"@aws-sdk/util-endpoints@3.391.0": + version "3.391.0" + resolved "https://registry.npmmirror.com/@aws-sdk/util-endpoints/-/util-endpoints-3.391.0.tgz#eb93e1331bd93773c05938001298a6c28e6db571" + integrity sha512-zv4sYDTQhNxyLoekcE02/nk3xvoo6yCHDy1kDJk0MFxOKaqUB+CvZdQBR4YBLSDlD4o4DUBmdYgKT58FfbM8sQ== + dependencies: + "@aws-sdk/types" "3.391.0" + tslib "^2.5.0" + "@aws-sdk/util-locate-window@^3.0.0": version "3.310.0" resolved "https://registry.npmmirror.com/@aws-sdk/util-locate-window/-/util-locate-window-3.310.0.tgz#b071baf050301adee89051032bd4139bba32cc40" @@ -1061,6 +1387,16 @@ bowser "^2.11.0" tslib "^2.5.0" +"@aws-sdk/util-user-agent-browser@3.391.0": + version "3.391.0" + resolved "https://registry.npmmirror.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.391.0.tgz#8ae8f4c9133be90a1ad9efe06b3e1f1ecdad24a6" + integrity sha512-6ipHOB1WdCBNeAMJauN7l2qNE0WLVaTNhkD290/ElXm1FHGTL8yw6lIDIjhIFO1bmbZxDiKApwDiG7ROhaJoxQ== + dependencies: + "@aws-sdk/types" "3.391.0" + "@smithy/types" "^2.2.0" + bowser "^2.11.0" + tslib "^2.5.0" + "@aws-sdk/util-user-agent-node@3.363.0": version "3.363.0" resolved "https://registry.npmmirror.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.363.0.tgz#9df26188a3d22694b4d06f5f40c489cb22fddb48" @@ -1070,6 +1406,16 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@aws-sdk/util-user-agent-node@3.391.0": + version "3.391.0" + resolved "https://registry.npmmirror.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.391.0.tgz#f15961e3ce64354912f16a644e1db27d2d431f42" + integrity sha512-PVvAK/Lf4BdB1eJIZtyFpGSslGQwKpYt9/hKs5NlR+qxBMXU9T0DnTqH4GiXZaazvXr7OUVWitIF2b7iKBMTow== + dependencies: + "@aws-sdk/types" "3.391.0" + "@smithy/node-config-provider" "^2.0.3" + "@smithy/types" "^2.2.0" + tslib "^2.5.0" + "@aws-sdk/util-utf8-browser@^3.0.0": version "3.259.0" resolved "https://registry.npmmirror.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz#3275a6f5eb334f96ca76635b961d3c50259fd9ff" @@ -2530,6 +2876,33 @@ version "6.0.2" resolved "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f" +"@chevrotain/cst-dts-gen@10.5.0": + version "10.5.0" + resolved "https://registry.npmmirror.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz#922ebd8cc59d97241bb01b1b17561a5c1ae0124e" + integrity sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw== + dependencies: + "@chevrotain/gast" "10.5.0" + "@chevrotain/types" "10.5.0" + lodash "4.17.21" + +"@chevrotain/gast@10.5.0": + version "10.5.0" + resolved "https://registry.npmmirror.com/@chevrotain/gast/-/gast-10.5.0.tgz#e4e614bc46d17a8892742f38e56cd33f1f3ad162" + integrity sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A== + dependencies: + "@chevrotain/types" "10.5.0" + lodash "4.17.21" + +"@chevrotain/types@10.5.0": + version "10.5.0" + resolved "https://registry.npmmirror.com/@chevrotain/types/-/types-10.5.0.tgz#52a97d74a8cfbc197f054636d93ecd8912d33d21" + integrity sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A== + +"@chevrotain/utils@10.5.0": + version "10.5.0" + resolved "https://registry.npmmirror.com/@chevrotain/utils/-/utils-10.5.0.tgz#0ee36f65b49b447fbac71b9e5af5c5c6c98ac057" + integrity sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ== + "@cnakazawa/watch@^1.0.3": version "1.0.4" resolved "https://registry.npmmirror.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" @@ -2695,6 +3068,14 @@ dependencies: chalk "^4.0.0" +"@contrast/fn-inspect@^3.3.0": + version "3.3.1" + resolved "https://registry.npmmirror.com/@contrast/fn-inspect/-/fn-inspect-3.3.1.tgz#3e8f7d297ce12742a4d54c7c7fea50a86e1ed5ed" + integrity sha512-BqsC5YslFxX/jgUzjAFEqnI0ngXXmUAFHUrhLSJu7lFYwTB7U1bLCUcjsZVnaO2bh0QDrmGAL/W0pe1Eu7PIIQ== + dependencies: + nan "^2.16.0" + node-gyp-build "^4.4.0" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -3467,6 +3848,25 @@ dependencies: fast-deep-equal "^3.1.3" +"@grpc/grpc-js@^1.8.10": + version "1.9.1" + resolved "https://registry.npmmirror.com/@grpc/grpc-js/-/grpc-js-1.9.1.tgz#d6df7943cd2875a4feaf725f85ff605c08ac245d" + integrity sha512-AvDEPQT4teS+J8++cTE5tku4rYCwpPwPguESJUummLs/Ug/O5Bouofnc1mxaDORmwA9QkrJ+PfRQ1Qs7adQgJg== + dependencies: + "@grpc/proto-loader" "^0.7.8" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.7.5", "@grpc/proto-loader@^0.7.8": + version "0.7.8" + resolved "https://registry.npmmirror.com/@grpc/proto-loader/-/proto-loader-0.7.8.tgz#c050bbeae5f000a1919507f195a1b094e218036e" + integrity sha512-GU12e2c8dmdXb7XUlOgYWZ2o2i+z9/VeACkxTA/zzAe2IjclC5PnVL0lpgjhrqfpDYHzM8B1TF6pqWegMYAzlA== + dependencies: + "@types/long" "^4.0.1" + lodash.camelcase "^4.3.0" + long "^4.0.0" + protobufjs "^7.2.4" + yargs "^17.7.2" + "@hapi/hoek@^11.0.2": version "11.0.2" resolved "https://registry.npmmirror.com/@hapi/hoek/-/hoek-11.0.2.tgz#cb3ea547daac7de5c9cf1d960c3f35c34f065427" @@ -4761,6 +5161,65 @@ semver "^7.3.5" tar "^6.1.11" +"@mrleebo/prisma-ast@^0.5.2": + version "0.5.2" + resolved "https://registry.npmmirror.com/@mrleebo/prisma-ast/-/prisma-ast-0.5.2.tgz#3df50be48bf0f1a97bf822de6a44c7c03a2067a7" + integrity sha512-v2jwtrLt/x5/MaF7Sucsz/do8tDUmiq3KA+UYdyZfr3OQ2IGXUtpNSXmdlvyRM+vQ7Abn/FxpLW/qqhZGB9vhQ== + dependencies: + chevrotain "^10.4.2" + +"@newrelic/aws-sdk@^6.0.0": + version "6.0.0" + resolved "https://registry.npmmirror.com/@newrelic/aws-sdk/-/aws-sdk-6.0.0.tgz#046241104394cafa3ade2cdff61c01f81b7df8ba" + integrity sha512-17DwEvyDS9pAkV5kBSGtm2oIQbII0qOSAXSlU51MLA0Vp/PxNDTCBBFVgpKbGyKpPfudIJMGvh4jAmlzH3xIng== + +"@newrelic/koa@^7.1.1": + version "7.2.0" + resolved "https://registry.npmmirror.com/@newrelic/koa/-/koa-7.2.0.tgz#85156cb5882b9e83a2e245b536516c4f91bdb71d" + integrity sha512-3y/CCOLJ6sEPTKyQAmBrBP5CfZ5ak8mWt+7mWjdbblOXQh20LEsrA/KQAh/ROcTh6rV8oxsubLZ3N13LIeIoVQ== + +"@newrelic/native-metrics@^9.0.1": + version "9.0.1" + resolved "https://registry.npmmirror.com/@newrelic/native-metrics/-/native-metrics-9.0.1.tgz#4cf2deca74209128dd003b7256b144acc0ae97f4" + integrity sha512-ZMCd6xW9PWhrWvg8Ik0oFU+XGFLbqRujh15qu3+7FJRI8163RBOD6SS8tsU0ydG8+LlaPDZQp/ODD4LvBXu5UA== + dependencies: + https-proxy-agent "^5.0.1" + nan "^2.17.0" + semver "^7.5.2" + +"@newrelic/security-agent@0.2.1": + version "0.2.1" + resolved "https://registry.npmmirror.com/@newrelic/security-agent/-/security-agent-0.2.1.tgz#4f1c97486fa16ec1d1e804f841c9eebf7a790e72" + integrity sha512-oFPnBO+BlJap/qC3r80NuiHmedXdjpWnVnzAlNjsSZoAT7qJM/29tip6ZX/Qmp17mZAIiOVJ2MkyQ5OXHXPdLw== + dependencies: + "@aws-sdk/client-lambda" "^3.363.0" + axios "0.21.4" + check-disk-space "3.3.1" + content-type "^1.0.5" + fast-safe-stringify "^2.1.1" + find-package-json "^1.2.0" + hash.js "^1.1.7" + html-entities "^2.3.6" + is-invalid-path "^1.0.2" + js-yaml "^4.1.0" + jsonschema "^1.4.1" + lodash "^4.17.21" + log4js "^6.9.1" + pretty-bytes "^5.6.0" + request-ip "^3.3.0" + ringbufferjs "^2.0.0" + semver "^7.5.4" + sync-request "^6.1.0" + unescape "^1.0.1" + unescape-js "^1.1.4" + uuid "^9.0.0" + ws "^7.5.9" + +"@newrelic/superagent@^6.0.0": + version "6.0.0" + resolved "https://registry.npmmirror.com/@newrelic/superagent/-/superagent-6.0.0.tgz#8aee871547ebea90fcb37f98ea38d820cc1bc67b" + integrity sha512-5nClQp9ACd4BvLusAgFHjjKLDgAaC+dKmIsRNOPC82LOLFaoOgxxtbecnDIJ0NWCKQS+WOdmXdgYutwH+e5dsA== + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.npmmirror.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -5056,6 +5515,59 @@ version "2.11.8" resolved "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + "@rc-component/color-picker@~1.4.0": version "1.4.1" resolved "https://registry.npmmirror.com/@rc-component/color-picker/-/color-picker-1.4.1.tgz#dcab0b660e9c4ed63a7582db68ed4a77c862cb93" @@ -5354,6 +5866,14 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/abort-controller@^2.0.4", "@smithy/abort-controller@^2.0.5": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/abort-controller/-/abort-controller-2.0.5.tgz#9602a9b362e84c0d043d820c4aba5d9b78028a84" + integrity sha512-byVZ2KWLMPYAZGKjRpniAzLcygJO4ruClZKdJTuB0eCB76ONFTdptBHlviHpAZXknRz7skYWPfcgO9v30A1SyA== + dependencies: + "@smithy/types" "^2.2.2" + tslib "^2.5.0" + "@smithy/config-resolver@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/config-resolver/-/config-resolver-1.0.1.tgz#da1c9f13f485e7cbb46e8aa62fe31c98deb6c403" @@ -5363,6 +5883,16 @@ "@smithy/util-middleware" "^1.0.1" tslib "^2.5.0" +"@smithy/config-resolver@^2.0.3", "@smithy/config-resolver@^2.0.5": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/config-resolver/-/config-resolver-2.0.5.tgz#d64c1c83a773ca5a038146d4b537c202b6c6bfaf" + integrity sha512-n0c2AXz+kjALY2FQr7Zy9zhYigXzboIh1AuUUVCqFBKFtdEvTwnwPXrTDoEehLiRTUHNL+4yzZ3s+D0kKYSLSg== + dependencies: + "@smithy/types" "^2.2.2" + "@smithy/util-config-provider" "^2.0.0" + "@smithy/util-middleware" "^2.0.0" + tslib "^2.5.0" + "@smithy/credential-provider-imds@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/credential-provider-imds/-/credential-provider-imds-1.0.1.tgz#650b0f1d839e65accf9d5d20e216b61eb17c8f87" @@ -5373,6 +5903,17 @@ "@smithy/url-parser" "^1.0.1" tslib "^2.5.0" +"@smithy/credential-provider-imds@^2.0.0", "@smithy/credential-provider-imds@^2.0.5": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/credential-provider-imds/-/credential-provider-imds-2.0.5.tgz#59e6f8d30beed9e966d418f47108bb4da371bbae" + integrity sha512-KFcf/e0meFkQNyteJ65f1G19sgUEY1e5zL7hyAEUPz2SEfBmC9B37WyRq87G3MEEsvmAWwCRu7nFFYUKtR3svQ== + dependencies: + "@smithy/node-config-provider" "^2.0.5" + "@smithy/property-provider" "^2.0.5" + "@smithy/types" "^2.2.2" + "@smithy/url-parser" "^2.0.5" + tslib "^2.5.0" + "@smithy/eventstream-codec@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/eventstream-codec/-/eventstream-codec-1.0.1.tgz#d35b29c501a39a7025be74f650f50dfe84471b85" @@ -5382,6 +5923,16 @@ "@smithy/util-hex-encoding" "^1.0.1" tslib "^2.5.0" +"@smithy/eventstream-codec@^2.0.4": + version "2.0.4" + resolved "https://registry.npmmirror.com/@smithy/eventstream-codec/-/eventstream-codec-2.0.4.tgz#6b823b2af455e5a2b731b89898402abf9e84dd3c" + integrity sha512-DkVLcQjhOxPj/4pf2hNj2kvOeoLczirHe57g7czMNJCUBvg9cpU9hNgqS37Y5sjdEtMSa2oTyCS5oeHZtKgoIw== + dependencies: + "@aws-crypto/crc32" "3.0.0" + "@smithy/types" "^2.2.1" + "@smithy/util-hex-encoding" "^2.0.0" + tslib "^2.5.0" + "@smithy/eventstream-serde-browser@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-1.0.1.tgz#d03e5262f6444708bf8313ab0e08179ab2718b0b" @@ -5390,6 +5941,15 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/eventstream-serde-browser@^2.0.3": + version "2.0.4" + resolved "https://registry.npmmirror.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-2.0.4.tgz#ce0d9c52a867728c8e23877ca5c9c113b8fb3c14" + integrity sha512-6eY3NZb0kHoHh1j0wK+nZwrEe0qnqUzTBEBr+auB/Dd2GJj6quFVRKG65UnuOym/fnGzM0Cc6vULb7fQqqhbiw== + dependencies: + "@smithy/eventstream-serde-universal" "^2.0.4" + "@smithy/types" "^2.2.1" + tslib "^2.5.0" + "@smithy/eventstream-serde-config-resolver@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-1.0.1.tgz#8e18dbca8874481ad3de5047a42e20eb77a5fa1b" @@ -5397,6 +5957,14 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/eventstream-serde-config-resolver@^2.0.3": + version "2.0.4" + resolved "https://registry.npmmirror.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-2.0.4.tgz#7a4fd423e105b9b225c01557b2ffcaf8dcebe1fd" + integrity sha512-OH+CxOki+MzMhasco3AL9bHw/6u2UcNz0XcP5kvmWTZngZTEiqEEnG6u20LHKu1HD3sDqsdK0n4hyelH5zce6A== + dependencies: + "@smithy/types" "^2.2.1" + tslib "^2.5.0" + "@smithy/eventstream-serde-node@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-1.0.1.tgz#5765af12cadcde7fcc75db1c161f51c50adb1caf" @@ -5405,6 +5973,15 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/eventstream-serde-node@^2.0.3": + version "2.0.4" + resolved "https://registry.npmmirror.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-2.0.4.tgz#47446a5901e86b86d136b7f32dc4b7696892ad51" + integrity sha512-O4KaVw0JdtWJ1Dbo0dBNa2wW5xEbDDTVbn/VY9hxLgS1TXHVPNYuvMP0Du+ZOJGmNul+1dOhIOx9kPBncS2MDg== + dependencies: + "@smithy/eventstream-serde-universal" "^2.0.4" + "@smithy/types" "^2.2.1" + tslib "^2.5.0" + "@smithy/eventstream-serde-universal@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-1.0.1.tgz#cb19a8381986a83b6d2c1ca0e72466140e0dfaa7" @@ -5413,6 +5990,15 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/eventstream-serde-universal@^2.0.4": + version "2.0.4" + resolved "https://registry.npmmirror.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-2.0.4.tgz#d6dcf111173379c73a29bf96819c1eb70b579fca" + integrity sha512-WHgAxBmWqKE6/LuwgbDZckS0ycN34drEMYQAbYGz5SK+Kpakl3zEeJ0DxnFXgdHdlVrlvaYtgzrMqfowH9of6g== + dependencies: + "@smithy/eventstream-codec" "^2.0.4" + "@smithy/types" "^2.2.1" + tslib "^2.5.0" + "@smithy/fetch-http-handler@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/fetch-http-handler/-/fetch-http-handler-1.0.1.tgz#0326ed6f4165a40a1979ed990f633c640846c4de" @@ -5423,6 +6009,17 @@ "@smithy/util-base64" "^1.0.1" tslib "^2.5.0" +"@smithy/fetch-http-handler@^2.0.3", "@smithy/fetch-http-handler@^2.0.5": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/fetch-http-handler/-/fetch-http-handler-2.0.5.tgz#822510720598b4306e7c71e839eea34b6928c66b" + integrity sha512-EzFoMowdBNy1VqtvkiXgPFEdosIAt4/4bgZ8uiDiUyfhmNXq/3bV+CagPFFBsgFOR/X2XK4zFZHRsoa7PNHVVg== + dependencies: + "@smithy/protocol-http" "^2.0.5" + "@smithy/querystring-builder" "^2.0.5" + "@smithy/types" "^2.2.2" + "@smithy/util-base64" "^2.0.0" + tslib "^2.5.0" + "@smithy/hash-node@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/hash-node/-/hash-node-1.0.1.tgz#9cffefd78f74a99d7f0d99f19596be1a163f70b1" @@ -5432,6 +6029,16 @@ "@smithy/util-utf8" "^1.0.1" tslib "^2.5.0" +"@smithy/hash-node@^2.0.3": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/hash-node/-/hash-node-2.0.5.tgz#f3558c1553f846148c3e5d10a815429e1b357668" + integrity sha512-mk551hIywBITT+kXruRNXk7f8Fy7DTzBjZJSr/V6nolYKmUHIG3w5QU6nO9qPYEQGKc/yEPtkpdS28ndeG93lA== + dependencies: + "@smithy/types" "^2.2.2" + "@smithy/util-buffer-from" "^2.0.0" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.5.0" + "@smithy/invalid-dependency@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/invalid-dependency/-/invalid-dependency-1.0.1.tgz#1dde6b71b91e29e3f347a56e9d246c0c48a81748" @@ -5439,12 +6046,27 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/invalid-dependency@^2.0.3": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/invalid-dependency/-/invalid-dependency-2.0.5.tgz#b07bdbc43403977b8bcae6de19a96e184f2eb655" + integrity sha512-0wEi+JT0hM+UUwrJVYbqjuGFhy5agY/zXyiN7BNAJ1XoCDjU5uaNSj8ekPWsXd/d4yM6NSe8UbPd8cOc1+3oBQ== + dependencies: + "@smithy/types" "^2.2.2" + tslib "^2.5.0" + "@smithy/is-array-buffer@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/is-array-buffer/-/is-array-buffer-1.0.1.tgz#42997d8321234438c141089120b5d3c9d68b0400" dependencies: tslib "^2.5.0" +"@smithy/is-array-buffer@^2.0.0": + version "2.0.0" + resolved "https://registry.npmmirror.com/@smithy/is-array-buffer/-/is-array-buffer-2.0.0.tgz#8fa9b8040651e7ba0b2f6106e636a91354ff7d34" + integrity sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug== + dependencies: + tslib "^2.5.0" + "@smithy/middleware-content-length@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/middleware-content-length/-/middleware-content-length-1.0.1.tgz#78747dfc7079e542c04675bb3ab1370c35511f7b" @@ -5453,6 +6075,15 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/middleware-content-length@^2.0.3": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/middleware-content-length/-/middleware-content-length-2.0.5.tgz#b2008c6b664c4c67fb255ef5a9fd5f4bd2c914f6" + integrity sha512-E7VwV5H02fgZIUGRli4GevBCAPvkyEI/fgl9SU47nPPi3DAAX3nEtUb8xfGbXjOcJ5BdSUoWWZn42tEd/blOqA== + dependencies: + "@smithy/protocol-http" "^2.0.5" + "@smithy/types" "^2.2.2" + tslib "^2.5.0" + "@smithy/middleware-endpoint@^1.0.1": version "1.0.2" resolved "https://registry.npmmirror.com/@smithy/middleware-endpoint/-/middleware-endpoint-1.0.2.tgz#6e8913e90bad7d73dc57f2a3dadfb0c4f045c9b5" @@ -5463,6 +6094,17 @@ "@smithy/util-middleware" "^1.0.1" tslib "^2.5.0" +"@smithy/middleware-endpoint@^2.0.3": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/middleware-endpoint/-/middleware-endpoint-2.0.5.tgz#6a16361dc527262958194e48343733ac6285776b" + integrity sha512-tyzDuoNTbsMQCq5Xkc4QOt6e2GACUllQIV8SQ5fc59FtOIV9/vbf58/GxVjZm2o8+MMbdDBANjTDZe/ijZKfyA== + dependencies: + "@smithy/middleware-serde" "^2.0.5" + "@smithy/types" "^2.2.2" + "@smithy/url-parser" "^2.0.5" + "@smithy/util-middleware" "^2.0.0" + tslib "^2.5.0" + "@smithy/middleware-retry@^1.0.1", "@smithy/middleware-retry@^1.0.2": version "1.0.3" resolved "https://registry.npmmirror.com/@smithy/middleware-retry/-/middleware-retry-1.0.3.tgz#95cac65da17a313c836c9f4a83aa348aad8625da" @@ -5475,6 +6117,19 @@ tslib "^2.5.0" uuid "^8.3.2" +"@smithy/middleware-retry@^2.0.3": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/middleware-retry/-/middleware-retry-2.0.5.tgz#bbf8858aeccdfe11837f89635cb6ce8a8e304518" + integrity sha512-ulIfbFyzQTVnJbLjUl1CTSi0etg6tej/ekwaLp0Gn8ybUkDkKYa+uB6CF/m2J5B6meRwyJlsryR+DjaOVyiicg== + dependencies: + "@smithy/protocol-http" "^2.0.5" + "@smithy/service-error-classification" "^2.0.0" + "@smithy/types" "^2.2.2" + "@smithy/util-middleware" "^2.0.0" + "@smithy/util-retry" "^2.0.0" + tslib "^2.5.0" + uuid "^8.3.2" + "@smithy/middleware-serde@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/middleware-serde/-/middleware-serde-1.0.1.tgz#b2f0e13abe3d0becb9c5b19d751fbb4b28bd9b1d" @@ -5482,12 +6137,27 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/middleware-serde@^2.0.3", "@smithy/middleware-serde@^2.0.5": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/middleware-serde/-/middleware-serde-2.0.5.tgz#3f3635cb437a3fba46cd1407d3adf53d41328574" + integrity sha512-in0AA5sous74dOfTGU9rMJBXJ0bDVNxwdXtEt5lh3FVd2sEyjhI+rqpLLRF1E4ixbw3RSEf80hfRpcPdjg4vvQ== + dependencies: + "@smithy/types" "^2.2.2" + tslib "^2.5.0" + "@smithy/middleware-stack@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/middleware-stack/-/middleware-stack-1.0.1.tgz#cfa8de79ca8bc09fc3c266a0e0241c639b571870" dependencies: tslib "^2.5.0" +"@smithy/middleware-stack@^2.0.0": + version "2.0.0" + resolved "https://registry.npmmirror.com/@smithy/middleware-stack/-/middleware-stack-2.0.0.tgz#cd9f442c2788b1ef0ea6b32236d80c76b3c342e9" + integrity sha512-31XC1xNF65nlbc16yuh3wwTudmqs6qy4EseQUGF8A/p2m/5wdd/cnXJqpniy/XvXVwkHPz/GwV36HqzHtIKATQ== + dependencies: + tslib "^2.5.0" + "@smithy/node-config-provider@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/node-config-provider/-/node-config-provider-1.0.1.tgz#c9b3c1f49a5238b398ad7ff1f2df28315672853f" @@ -5497,6 +6167,16 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/node-config-provider@^2.0.3", "@smithy/node-config-provider@^2.0.5": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/node-config-provider/-/node-config-provider-2.0.5.tgz#239a6281e1d0bc2a0dd8fdab7826bacd25dfbf00" + integrity sha512-LRtjV9WkhONe2lVy+ipB/l1GX60ybzBmFyeRUoLUXWKdnZ3o81jsnbKzMK8hKq8eFSWPk+Lmyx6ZzCQabGeLxg== + dependencies: + "@smithy/property-provider" "^2.0.5" + "@smithy/shared-ini-file-loader" "^2.0.5" + "@smithy/types" "^2.2.2" + tslib "^2.5.0" + "@smithy/node-http-handler@^1.0.1", "@smithy/node-http-handler@^1.0.2": version "1.0.2" resolved "https://registry.npmmirror.com/@smithy/node-http-handler/-/node-http-handler-1.0.2.tgz#fe27a3a5d83874a23d24c29cd2bf0258a5e4730b" @@ -5507,6 +6187,17 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/node-http-handler@^2.0.3", "@smithy/node-http-handler@^2.0.5": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/node-http-handler/-/node-http-handler-2.0.5.tgz#19c1bdd4d61502bc9c793dddb8ce995626ca6585" + integrity sha512-lZm5DZf4b3V0saUw9WTC4/du887P6cy2fUyQgQQKRRV6OseButyD5yTzeMmXE53CaXJBMBsUvvIQ0hRVxIq56w== + dependencies: + "@smithy/abort-controller" "^2.0.5" + "@smithy/protocol-http" "^2.0.5" + "@smithy/querystring-builder" "^2.0.5" + "@smithy/types" "^2.2.2" + tslib "^2.5.0" + "@smithy/property-provider@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/property-provider/-/property-provider-1.0.1.tgz#b4c2f41313fcfe33d64f6198f91fc9edd96dc28d" @@ -5514,6 +6205,14 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/property-provider@^2.0.0", "@smithy/property-provider@^2.0.5": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/property-provider/-/property-provider-2.0.5.tgz#7cc88bc56706a4758076754a71c6a9ebf5daa8a7" + integrity sha512-cAFSUhX6aiHcmpWfrCLKvwBtgN1F6A0N8qY/8yeSi0LRLmhGqsY1/YTxFE185MCVzYbqBGXVr9TBv4RUcIV4rA== + dependencies: + "@smithy/types" "^2.2.2" + tslib "^2.5.0" + "@smithy/protocol-http@^1.0.1", "@smithy/protocol-http@^1.1.0": version "1.1.0" resolved "https://registry.npmmirror.com/@smithy/protocol-http/-/protocol-http-1.1.0.tgz#caf22e01cb825d7490a4915e03d6fa64954ff535" @@ -5521,6 +6220,14 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/protocol-http@^2.0.3", "@smithy/protocol-http@^2.0.5": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/protocol-http/-/protocol-http-2.0.5.tgz#ff7779fc8fcd3fe52e71fd07565b518f0937e8ba" + integrity sha512-d2hhHj34mA2V86doiDfrsy2fNTnUOowGaf9hKb0hIPHqvcnShU4/OSc4Uf1FwHkAdYF3cFXTrj5VGUYbEuvMdw== + dependencies: + "@smithy/types" "^2.2.2" + tslib "^2.5.0" + "@smithy/querystring-builder@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/querystring-builder/-/querystring-builder-1.0.1.tgz#c8e9ca45c072763ad34b34a4aaf6a85eb37eafd3" @@ -5529,6 +6236,15 @@ "@smithy/util-uri-escape" "^1.0.1" tslib "^2.5.0" +"@smithy/querystring-builder@^2.0.5": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/querystring-builder/-/querystring-builder-2.0.5.tgz#c5a873769de56ef57ae3b4d2c58fc7f68184a89c" + integrity sha512-4DCX9krxLzATj+HdFPC3i8pb7XTAWzzKqSw8aTZMjXjtQY+vhe4azMAqIvbb6g7JKwIkmkRAjK6EXO3YWSnJVQ== + dependencies: + "@smithy/types" "^2.2.2" + "@smithy/util-uri-escape" "^2.0.0" + tslib "^2.5.0" + "@smithy/querystring-parser@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/querystring-parser/-/querystring-parser-1.0.1.tgz#bf5e326debaecb9e1db44972f1eb87d64e8915e8" @@ -5536,10 +6252,23 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/querystring-parser@^2.0.5": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/querystring-parser/-/querystring-parser-2.0.5.tgz#aec6733ed4497402634978e7026d0d00661594d6" + integrity sha512-C2stCULH0r54KBksv3AWcN8CLS3u9+WsEW8nBrvctrJ5rQTNa1waHkffpVaiKvcW2nP0aIMBPCobD/kYf/q9mA== + dependencies: + "@smithy/types" "^2.2.2" + tslib "^2.5.0" + "@smithy/service-error-classification@^1.0.2": version "1.0.2" resolved "https://registry.npmmirror.com/@smithy/service-error-classification/-/service-error-classification-1.0.2.tgz#9145b66b7935fbbde43e53c82853fc96a448a552" +"@smithy/service-error-classification@^2.0.0": + version "2.0.0" + resolved "https://registry.npmmirror.com/@smithy/service-error-classification/-/service-error-classification-2.0.0.tgz#bbce07c9c529d9333d40db881fd4a1795dd84892" + integrity sha512-2z5Nafy1O0cTf69wKyNjGW/sNVMiqDnb4jgwfMG8ye8KnFJ5qmJpDccwIbJNhXIfbsxTg9SEec2oe1cexhMJvw== + "@smithy/shared-ini-file-loader@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-1.0.1.tgz#6bc72721e0b1148b199bd2bb21b4d5d3083534bb" @@ -5547,6 +6276,14 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/shared-ini-file-loader@^2.0.0", "@smithy/shared-ini-file-loader@^2.0.5": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.0.5.tgz#c2b28b499f2b9928e892a80fcdeb259b2938475c" + integrity sha512-Mvtk6FwMtfbKRC4YuSsIqRYp9WTxsSUJVVo2djgyhcacKGMqicHDWSAmgy3sDrKv+G/G6xTZCPwm6pJARtdxVg== + dependencies: + "@smithy/types" "^2.2.2" + tslib "^2.5.0" + "@smithy/signature-v4@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/signature-v4/-/signature-v4-1.0.1.tgz#03e9d8e3fd5d4f449fc3c188402f5655231d91aa" @@ -5560,6 +6297,20 @@ "@smithy/util-utf8" "^1.0.1" tslib "^2.5.0" +"@smithy/signature-v4@^2.0.0": + version "2.0.4" + resolved "https://registry.npmmirror.com/@smithy/signature-v4/-/signature-v4-2.0.4.tgz#97d553b9e2a5355b12bdbc0dc97031f04b1fcf42" + integrity sha512-y2xblkS0hb44QJDn9YjPp5aRFYSiI7w0bI3tATE3ybOrII2fppqD0SE3zgvew/B/3rTunuiCW+frTD0W4UYb9Q== + dependencies: + "@smithy/eventstream-codec" "^2.0.4" + "@smithy/is-array-buffer" "^2.0.0" + "@smithy/types" "^2.2.1" + "@smithy/util-hex-encoding" "^2.0.0" + "@smithy/util-middleware" "^2.0.0" + "@smithy/util-uri-escape" "^2.0.0" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.5.0" + "@smithy/smithy-client@^1.0.2", "@smithy/smithy-client@^1.0.3": version "1.0.3" resolved "https://registry.npmmirror.com/@smithy/smithy-client/-/smithy-client-1.0.3.tgz#f31bf64fc4a21fc42171120635e6af2b58dabc22" @@ -5569,12 +6320,29 @@ "@smithy/util-stream" "^1.0.1" tslib "^2.5.0" +"@smithy/smithy-client@^2.0.3": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/smithy-client/-/smithy-client-2.0.5.tgz#7941449f146d2c61d34670779d77d4a085141bc1" + integrity sha512-kCTFr8wfOAWKDzGvfBElc6shHigWtHNhMQ1IbosjC4jOlayFyZMSs2PysKB+Ox/dhQ41KqOzgVjgiQ+PyWqHMQ== + dependencies: + "@smithy/middleware-stack" "^2.0.0" + "@smithy/types" "^2.2.2" + "@smithy/util-stream" "^2.0.5" + tslib "^2.5.0" + "@smithy/types@^1.0.0", "@smithy/types@^1.1.0": version "1.1.0" resolved "https://registry.npmmirror.com/@smithy/types/-/types-1.1.0.tgz#f30a23202c97634cca5c1ac955a9bf149c955226" dependencies: tslib "^2.5.0" +"@smithy/types@^2.2.0", "@smithy/types@^2.2.1", "@smithy/types@^2.2.2": + version "2.2.2" + resolved "https://registry.npmmirror.com/@smithy/types/-/types-2.2.2.tgz#bd8691eb92dd07ac33b83e0e1c45f283502b1bf7" + integrity sha512-4PS0y1VxDnELGHGgBWlDksB2LJK8TG8lcvlWxIsgR+8vROI7Ms8h1P4FQUx+ftAX2QZv5g1CJCdhdRmQKyonyw== + dependencies: + tslib "^2.5.0" + "@smithy/url-parser@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/url-parser/-/url-parser-1.0.1.tgz#a7e52ebb4c8c1d19aa47397b7d78ebcdf767013a" @@ -5583,6 +6351,15 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/url-parser@^2.0.3", "@smithy/url-parser@^2.0.5": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/url-parser/-/url-parser-2.0.5.tgz#09fa623076bb5861892930628bf368d5c79fd7d9" + integrity sha512-OdMBvZhpckQSkugCXNJQCvqJ71wE7Ftxce92UOQLQ9pwF6hoS5PLL7wEfpnuEXtStzBqJYkzu1C1ZfjuFGOXAA== + dependencies: + "@smithy/querystring-parser" "^2.0.5" + "@smithy/types" "^2.2.2" + tslib "^2.5.0" + "@smithy/util-base64@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/util-base64/-/util-base64-1.0.1.tgz#69f682a542ca57ffb732da6f2e9309d238d52d84" @@ -5590,18 +6367,40 @@ "@smithy/util-buffer-from" "^1.0.1" tslib "^2.5.0" +"@smithy/util-base64@^2.0.0": + version "2.0.0" + resolved "https://registry.npmmirror.com/@smithy/util-base64/-/util-base64-2.0.0.tgz#1beeabfb155471d1d41c8d0603be1351f883c444" + integrity sha512-Zb1E4xx+m5Lud8bbeYi5FkcMJMnn+1WUnJF3qD7rAdXpaL7UjkFQLdmW5fHadoKbdHpwH9vSR8EyTJFHJs++tA== + dependencies: + "@smithy/util-buffer-from" "^2.0.0" + tslib "^2.5.0" + "@smithy/util-body-length-browser@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/util-body-length-browser/-/util-body-length-browser-1.0.1.tgz#42b3042a4a58119aa97e9b85c1f7bb11dcc3b472" dependencies: tslib "^2.5.0" +"@smithy/util-body-length-browser@^2.0.0": + version "2.0.0" + resolved "https://registry.npmmirror.com/@smithy/util-body-length-browser/-/util-body-length-browser-2.0.0.tgz#5447853003b4c73da3bc5f3c5e82c21d592d1650" + integrity sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg== + dependencies: + tslib "^2.5.0" + "@smithy/util-body-length-node@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/util-body-length-node/-/util-body-length-node-1.0.1.tgz#7909b05c8d5963ce44b1fc424bb1cdda7615af94" dependencies: tslib "^2.5.0" +"@smithy/util-body-length-node@^2.0.0": + version "2.1.0" + resolved "https://registry.npmmirror.com/@smithy/util-body-length-node/-/util-body-length-node-2.1.0.tgz#313a5f7c5017947baf5fa018bfc22628904bbcfa" + integrity sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw== + dependencies: + tslib "^2.5.0" + "@smithy/util-buffer-from@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/util-buffer-from/-/util-buffer-from-1.0.1.tgz#ec73eb769b246062ae86d037c2f3d54868a7c803" @@ -5609,12 +6408,27 @@ "@smithy/is-array-buffer" "^1.0.1" tslib "^2.5.0" +"@smithy/util-buffer-from@^2.0.0": + version "2.0.0" + resolved "https://registry.npmmirror.com/@smithy/util-buffer-from/-/util-buffer-from-2.0.0.tgz#7eb75d72288b6b3001bc5f75b48b711513091deb" + integrity sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw== + dependencies: + "@smithy/is-array-buffer" "^2.0.0" + tslib "^2.5.0" + "@smithy/util-config-provider@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/util-config-provider/-/util-config-provider-1.0.1.tgz#597d4f9a0bc9eab62c0d082b8e0772f92d85f99c" dependencies: tslib "^2.5.0" +"@smithy/util-config-provider@^2.0.0": + version "2.0.0" + resolved "https://registry.npmmirror.com/@smithy/util-config-provider/-/util-config-provider-2.0.0.tgz#4dd6a793605559d94267312fd06d0f58784b4c38" + integrity sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg== + dependencies: + tslib "^2.5.0" + "@smithy/util-defaults-mode-browser@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-1.0.1.tgz#609074d664dacb7fe6567d8fc259a85da2d4d262" @@ -5624,6 +6438,16 @@ bowser "^2.11.0" tslib "^2.5.0" +"@smithy/util-defaults-mode-browser@^2.0.3": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.0.5.tgz#36d5424749d324bd69f37c74ea20a183f8c2286e" + integrity sha512-yciP6TPttLsj731aHTvekgyuCGXQrEAJibEwEWAh3kzaDsfGAVCuZSBlyvC2Dl3TZmHKCOQwHV8mIE7KQCTPuQ== + dependencies: + "@smithy/property-provider" "^2.0.5" + "@smithy/types" "^2.2.2" + bowser "^2.11.0" + tslib "^2.5.0" + "@smithy/util-defaults-mode-node@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-1.0.1.tgz#5efe0bc575ec2253a4723d05a6f1fd28a567996e" @@ -5635,18 +6459,44 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/util-defaults-mode-node@^2.0.3": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.0.5.tgz#504dd39a603fd2d67e53537c794dd57e6541baae" + integrity sha512-M07t99rWasXt+IaDZDyP3BkcoEm/mgIE1RIMASrE49LKSNxaVN7PVcgGc77+4uu2kzBAyqJKy79pgtezuknyjQ== + dependencies: + "@smithy/config-resolver" "^2.0.5" + "@smithy/credential-provider-imds" "^2.0.5" + "@smithy/node-config-provider" "^2.0.5" + "@smithy/property-provider" "^2.0.5" + "@smithy/types" "^2.2.2" + tslib "^2.5.0" + "@smithy/util-hex-encoding@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/util-hex-encoding/-/util-hex-encoding-1.0.1.tgz#78778f664fa2624b2892d73e82df29b6d8e02380" dependencies: tslib "^2.5.0" +"@smithy/util-hex-encoding@^2.0.0": + version "2.0.0" + resolved "https://registry.npmmirror.com/@smithy/util-hex-encoding/-/util-hex-encoding-2.0.0.tgz#0aa3515acd2b005c6d55675e377080a7c513b59e" + integrity sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA== + dependencies: + tslib "^2.5.0" + "@smithy/util-middleware@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/util-middleware/-/util-middleware-1.0.1.tgz#8c1b04e9eb60e135a7083fc3ff729f969cd38d6a" dependencies: tslib "^2.5.0" +"@smithy/util-middleware@^2.0.0": + version "2.0.0" + resolved "https://registry.npmmirror.com/@smithy/util-middleware/-/util-middleware-2.0.0.tgz#706681d4a1686544a2275f68266304233f372c99" + integrity sha512-eCWX4ECuDHn1wuyyDdGdUWnT4OGyIzV0LN1xRttBFMPI9Ff/4heSHVxneyiMtOB//zpXWCha1/SWHJOZstG7kA== + dependencies: + tslib "^2.5.0" + "@smithy/util-retry@^1.0.1", "@smithy/util-retry@^1.0.2", "@smithy/util-retry@^1.0.3": version "1.0.3" resolved "https://registry.npmmirror.com/@smithy/util-retry/-/util-retry-1.0.3.tgz#c453af2ff19d0a51c9ea96f1d5d18625570f8073" @@ -5654,6 +6504,14 @@ "@smithy/service-error-classification" "^1.0.2" tslib "^2.5.0" +"@smithy/util-retry@^2.0.0": + version "2.0.0" + resolved "https://registry.npmmirror.com/@smithy/util-retry/-/util-retry-2.0.0.tgz#7ac5d5f12383a9d9b2a43f9ff25f3866c8727c24" + integrity sha512-/dvJ8afrElasuiiIttRJeoS2sy8YXpksQwiM/TcepqdRVp7u4ejd9C4IQURHNjlfPUT7Y6lCDSa2zQJbdHhVTg== + dependencies: + "@smithy/service-error-classification" "^2.0.0" + tslib "^2.5.0" + "@smithy/util-stream@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/util-stream/-/util-stream-1.0.1.tgz#3a8ec036ab429a150f2e36fb8a4b947255a7a258" @@ -5667,12 +6525,33 @@ "@smithy/util-utf8" "^1.0.1" tslib "^2.5.0" +"@smithy/util-stream@^2.0.3", "@smithy/util-stream@^2.0.5": + version "2.0.5" + resolved "https://registry.npmmirror.com/@smithy/util-stream/-/util-stream-2.0.5.tgz#a59f6e5327dfa23c3302f578ea023674fc7fa42f" + integrity sha512-ylx27GwI05xLpYQ4hDIfS15vm+wYjNN0Sc2P0FxuzgRe8v0BOLHppGIQ+Bezcynk8C9nUzsUue3TmtRhjut43g== + dependencies: + "@smithy/fetch-http-handler" "^2.0.5" + "@smithy/node-http-handler" "^2.0.5" + "@smithy/types" "^2.2.2" + "@smithy/util-base64" "^2.0.0" + "@smithy/util-buffer-from" "^2.0.0" + "@smithy/util-hex-encoding" "^2.0.0" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.5.0" + "@smithy/util-uri-escape@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/util-uri-escape/-/util-uri-escape-1.0.1.tgz#16422dc996767c459b12bf045a942e4044a32952" dependencies: tslib "^2.5.0" +"@smithy/util-uri-escape@^2.0.0": + version "2.0.0" + resolved "https://registry.npmmirror.com/@smithy/util-uri-escape/-/util-uri-escape-2.0.0.tgz#19955b1a0f517a87ae77ac729e0e411963dfda95" + integrity sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw== + dependencies: + tslib "^2.5.0" + "@smithy/util-utf8@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/util-utf8/-/util-utf8-1.0.1.tgz#6ce31e1f165212b1060aae11f7de4fe06ce8533a" @@ -5680,6 +6559,14 @@ "@smithy/util-buffer-from" "^1.0.1" tslib "^2.5.0" +"@smithy/util-utf8@^2.0.0": + version "2.0.0" + resolved "https://registry.npmmirror.com/@smithy/util-utf8/-/util-utf8-2.0.0.tgz#b4da87566ea7757435e153799df9da717262ad42" + integrity sha512-rctU1VkziY84n5OXe3bPNpKR001ZCME2JCaBBFgtiM2hfKbHFudc/BkMuPab8hRbLd0j3vbnBTTZ1igBf0wgiQ== + dependencies: + "@smithy/util-buffer-from" "^2.0.0" + tslib "^2.5.0" + "@smithy/util-waiter@^1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@smithy/util-waiter/-/util-waiter-1.0.1.tgz#d0ef960684f0755028abd02b8866e9b377363170" @@ -5688,6 +6575,15 @@ "@smithy/types" "^1.1.0" tslib "^2.5.0" +"@smithy/util-waiter@^2.0.3": + version "2.0.4" + resolved "https://registry.npmmirror.com/@smithy/util-waiter/-/util-waiter-2.0.4.tgz#29b302386d95fa596be6913de0e292faced67ee2" + integrity sha512-NAzHgewL+sIJw9vlgR4m8btJiu1u0vuQRNRT7Bd5B66h02deFMmOaw1zeGePORZa7zyUwNZ2J5ZPkKzq4ced7Q== + dependencies: + "@smithy/abort-controller" "^2.0.4" + "@smithy/types" "^2.2.1" + tslib "^2.5.0" + "@stackblitz/sdk@^1.8.1": version "1.9.0" resolved "https://registry.npmmirror.com/@stackblitz/sdk/-/sdk-1.9.0.tgz#b5174f3f45a51b6c1b9e67f1ef4e2e783ab105e9" @@ -6060,6 +6956,13 @@ version "4.3.5" resolved "https://registry.npmmirror.com/@types/chai/-/chai-4.3.5.tgz#ae69bcbb1bebb68c4ac0b11e9d8ed04526b3562b" +"@types/concat-stream@^1.6.0": + version "1.6.1" + resolved "https://registry.npmmirror.com/@types/concat-stream/-/concat-stream-1.6.1.tgz#24bcfc101ecf68e886aaedce60dfd74b632a1b74" + integrity sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA== + dependencies: + "@types/node" "*" + "@types/connect@*": version "3.4.35" resolved "https://registry.npmmirror.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" @@ -6325,6 +7228,13 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/form-data@0.0.33": + version "0.0.33" + resolved "https://registry.npmmirror.com/@types/form-data/-/form-data-0.0.33.tgz#c9ac85b2a5fd18435b8c85d9ecb50e6d6c893ff8" + integrity sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw== + dependencies: + "@types/node" "*" + "@types/fs-extra@^11.0.1": version "11.0.1" resolved "https://registry.npmmirror.com/@types/fs-extra/-/fs-extra-11.0.1.tgz#f542ec47810532a8a252127e6e105f487e0a6ea5" @@ -6498,6 +7408,11 @@ version "4.14.195" resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632" +"@types/long@^4.0.1": + version "4.0.2" + resolved "https://registry.npmmirror.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== + "@types/luxon@*": version "3.3.0" resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.3.0.tgz#a61043a62c0a72696c73a0a305c544c96501e006" @@ -6564,6 +7479,16 @@ version "20.3.3" resolved "https://registry.npmmirror.com/@types/node/-/node-20.3.3.tgz#329842940042d2b280897150e023e604d11657d6" +"@types/node@>=12.12.47", "@types/node@>=13.7.0": + version "20.5.3" + resolved "https://registry.npmmirror.com/@types/node/-/node-20.5.3.tgz#fa52c147f405d56b2f1dd8780d840aa87ddff629" + integrity sha512-ITI7rbWczR8a/S6qjAW7DMqxqFMjjTo61qZVWJ1ubPvbIQsL5D/TvwjYEalM8Kthpe3hTzOGrF2TGbAu2uyqeA== + +"@types/node@^10.0.3": + version "10.17.60" + resolved "https://registry.npmmirror.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" + integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== + "@types/node@^12.0.2": version "12.20.55" resolved "https://registry.npmmirror.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" @@ -6576,6 +7501,11 @@ version "17.0.45" resolved "https://registry.npmmirror.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" +"@types/node@^8.0.0": + version "8.10.66" + resolved "https://registry.npmmirror.com/@types/node/-/node-8.10.66.tgz#dd035d409df322acc83dff62a602f12a5783bbb3" + integrity sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw== + "@types/nodemailer@6.4.4": version "6.4.4" resolved "https://registry.npmmirror.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b" @@ -6616,7 +7546,7 @@ version "1.5.5" resolved "https://registry.npmmirror.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" -"@types/qs@*": +"@types/qs@*", "@types/qs@^6.2.31": version "6.9.7" resolved "https://registry.npmmirror.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" @@ -6696,6 +7626,13 @@ "@types/mime" "^1" "@types/node" "*" +"@types/serve-handler@^6.1.1": + version "6.1.1" + resolved "https://registry.npmmirror.com/@types/serve-handler/-/serve-handler-6.1.1.tgz#629dc9a62b201ab79a216e1e46e162aa4c8d1455" + integrity sha512-bIwSmD+OV8w0t2e7EWsuQYlGoS1o5aEdVktgkXaa43Zm0qVWi21xaSRb3DQA1UXD+DJ5bRq1Rgu14ZczB+CjIQ== + dependencies: + "@types/node" "*" + "@types/serve-static@*": version "1.15.2" resolved "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.2.tgz#3e5419ecd1e40e7405d34093f10befb43f63381a" @@ -6755,6 +7692,13 @@ version "3.0.0" resolved "https://registry.npmmirror.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52" +"@types/ws@^8.5.5": + version "8.5.5" + resolved "https://registry.npmmirror.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" + integrity sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg== + dependencies: + "@types/node" "*" + "@types/xml-crypto@^1.4.2": version "1.4.2" resolved "https://registry.npmmirror.com/@types/xml-crypto/-/xml-crypto-1.4.2.tgz#5ea7ef970f525ae8fe1e2ce0b3d40da1e3b279ae" @@ -6983,6 +7927,11 @@ "@typescript-eslint/types" "6.2.0" eslint-visitor-keys "^3.4.1" +"@tyriar/fibonacci-heap@^2.0.7": + version "2.0.9" + resolved "https://registry.npmmirror.com/@tyriar/fibonacci-heap/-/fibonacci-heap-2.0.9.tgz#df3dcbdb1b9182168601f6318366157ee16666e9" + integrity sha512-bYuSNomfn4hu2tPiDN+JZtnzCpSpbJ/PNeulmocDy3xN2X5OkJL65zo6rPZp65cPPhLF9vfT/dgE+RtFRCSxOA== + "@umijs/ast@4.0.72": version "4.0.72" resolved "https://registry.npmmirror.com/@umijs/ast/-/ast-4.0.72.tgz#54cf0d5edc5a09b06a2dff51d978cb3dc6bc6e11" @@ -7457,6 +8406,13 @@ agent-base@6, agent-base@^6.0.0, agent-base@^6.0.2: dependencies: debug "4" +agent-base@^7.0.2: + version "7.1.0" + resolved "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434" + integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== + dependencies: + debug "^4.3.4" + agentkeepalive@^3.3.0, agentkeepalive@^3.4.1: version "3.5.2" resolved "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67" @@ -8076,7 +9032,7 @@ arrify@^2.0.1: version "2.0.1" resolved "https://registry.npmmirror.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" -asap@^2.0.0, asap@~2.0.3: +asap@^2.0.0, asap@~2.0.3, asap@~2.0.6: version "2.0.6" resolved "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -8247,6 +9203,12 @@ axios-mock-adapter@^1.20.0: fast-deep-equal "^3.1.3" is-buffer "^2.0.5" +axios@0.21.4, axios@^0.21.0: + version "0.21.4" + resolved "https://registry.npmmirror.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + dependencies: + follow-redirects "^1.14.0" + axios@^0.18.1: version "0.18.1" resolved "https://registry.npmmirror.com/axios/-/axios-0.18.1.tgz#ff3f0de2e7b5d180e757ad98000f1081b87bcea3" @@ -8254,12 +9216,6 @@ axios@^0.18.1: follow-redirects "1.5.10" is-buffer "^2.0.2" -axios@^0.21.0: - version "0.21.4" - resolved "https://registry.npmmirror.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" - dependencies: - follow-redirects "^1.14.0" - axios@^0.26.1: version "0.26.1" resolved "https://registry.npmmirror.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" @@ -8552,6 +9508,11 @@ big.js@^5.2.2: version "5.2.2" resolved "https://registry.npmmirror.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" +bignumber.js@^9.0.0: + version "9.1.1" + resolved "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6" + integrity sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig== + binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -9098,7 +10059,7 @@ capture-stack-trace@^1.0.0: version "1.0.2" resolved "https://registry.npmmirror.com/capture-stack-trace/-/capture-stack-trace-1.0.2.tgz#1c43f6b059d4249e7f3f8724f15f048b927d3a8a" -caseless@~0.12.0: +caseless@^0.12.0, caseless@~0.12.0: version "0.12.0" resolved "https://registry.npmmirror.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -9216,6 +10177,11 @@ charm@~0.1.1: version "0.1.2" resolved "https://registry.npmmirror.com/charm/-/charm-0.1.2.tgz#06c21eed1a1b06aeb67553cdc53e23274bac2296" +check-disk-space@3.3.1: + version "3.3.1" + resolved "https://registry.npmmirror.com/check-disk-space/-/check-disk-space-3.3.1.tgz#10c4c8706fdd16d3e5c3572a16aa95efd0b4d40b" + integrity sha512-iOrT8yCZjSnyNZ43476FE2rnssvgw5hnuwOM0hm8Nj1qa0v4ieUUEbCyxxsEliaoDUb/75yCOL71zkDiDBLbMQ== + check-error@^1.0.2: version "1.0.2" resolved "https://registry.npmmirror.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" @@ -9243,6 +10209,18 @@ cheerio@^1.0.0-rc.3: parse5 "^7.0.0" parse5-htmlparser2-tree-adapter "^7.0.0" +chevrotain@^10.4.2: + version "10.5.0" + resolved "https://registry.npmmirror.com/chevrotain/-/chevrotain-10.5.0.tgz#9c1dc62ef0753bb562dbe521b5f72d041bad624e" + integrity sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A== + dependencies: + "@chevrotain/cst-dts-gen" "10.5.0" + "@chevrotain/gast" "10.5.0" + "@chevrotain/types" "10.5.0" + "@chevrotain/utils" "10.5.0" + lodash "4.17.21" + regexp-to-ast "0.5.0" + china-division@^2.4.0: version "2.6.1" resolved "https://registry.npmmirror.com/china-division/-/china-division-2.6.1.tgz#a3e3e4609e81077cc97443f78c713e03d66d7fc2" @@ -9759,7 +10737,7 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@^1.5.0, concat-stream@^1.5.2: +concat-stream@^1.5.0, concat-stream@^1.5.2, concat-stream@^1.6.0, concat-stream@^1.6.2: version "1.6.2" resolved "https://registry.npmmirror.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" dependencies: @@ -9864,7 +10842,7 @@ content-disposition@~0.5.2: dependencies: safe-buffer "5.2.1" -content-type@^1.0.2, content-type@^1.0.4: +content-type@^1.0.2, content-type@^1.0.4, content-type@^1.0.5: version "1.0.5" resolved "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" @@ -10862,6 +11840,11 @@ date-fns@^2.29.1: dependencies: "@babel/runtime" "^7.21.0" +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.npmmirror.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== + dateformat@^2.0.0: version "2.2.0" resolved "https://registry.npmmirror.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" @@ -12856,7 +13839,7 @@ flat@^5.0.2: version "5.0.2" resolved "https://registry.npmmirror.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" -flatted@^3.1.0: +flatted@^3.1.0, flatted@^3.2.7: version "3.2.7" resolved "https://registry.npmmirror.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" @@ -12926,6 +13909,15 @@ fork-ts-checker-webpack-plugin@8.0.0: semver "^7.3.5" tapable "^2.2.1" +form-data@^2.2.0: + version "2.5.1" + resolved "https://registry.npmmirror.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + form-data@^3.0.0: version "3.0.1" resolved "https://registry.npmmirror.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" @@ -13225,6 +14217,11 @@ get-pkg-repo@^4.0.0: through2 "^2.0.0" yargs "^16.2.0" +get-port@^3.1.0: + version "3.2.0" + resolved "https://registry.npmmirror.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" + integrity sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg== + get-port@^5.1.1: version "5.1.1" resolved "https://registry.npmmirror.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" @@ -13754,7 +14751,7 @@ hash-base@^3.0.0: readable-stream "^3.6.0" safe-buffer "^5.2.0" -hash.js@^1.0.0, hash.js@^1.0.3: +hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: version "1.1.7" resolved "https://registry.npmmirror.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" dependencies: @@ -13999,7 +14996,7 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" -html-entities@^2.1.0: +html-entities@^2.1.0, html-entities@^2.3.6: version "2.4.0" resolved "https://registry.npmmirror.com/html-entities/-/html-entities-2.4.0.tgz#edd0cee70402584c8c76cc2c0556db09d1f45061" @@ -14098,6 +15095,16 @@ http-assert@^1.3.0: deep-equal "~1.0.1" http-errors "~1.8.0" +http-basic@^8.1.1: + version "8.1.3" + resolved "https://registry.npmmirror.com/http-basic/-/http-basic-8.1.3.tgz#a7cabee7526869b9b710136970805b1004261bbf" + integrity sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw== + dependencies: + caseless "^0.12.0" + concat-stream "^1.6.2" + http-response-object "^3.0.1" + parse-cache-control "^1.0.1" + http-cache-semantics@^3.8.0: version "3.8.1" resolved "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" @@ -14154,6 +15161,13 @@ http-proxy-agent@^4.0.0, http-proxy-agent@^4.0.1: agent-base "6" debug "4" +http-response-object@^3.0.1: + version "3.0.2" + resolved "https://registry.npmmirror.com/http-response-object/-/http-response-object-3.0.2.tgz#7f435bb210454e4360d074ef1f989d5ea8aa9810" + integrity sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA== + dependencies: + "@types/node" "^10.0.3" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.npmmirror.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -14166,7 +15180,7 @@ https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.npmmirror.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" -https-proxy-agent@5, https-proxy-agent@^5.0.0: +https-proxy-agent@5, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" dependencies: @@ -14180,6 +15194,14 @@ https-proxy-agent@^2.1.0: agent-base "^4.3.0" debug "^3.1.0" +https-proxy-agent@^7.0.1: + version "7.0.1" + resolved "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz#0277e28f13a07d45c663633841e20a40aaafe0ab" + integrity sha512-Eun8zV0kcYS1g19r78osiQLEFIRspRUDd9tIfBCTBPBeMieF/EsJNL8VI3xOIdYRDEkjQnqOYPsZ2DsWsVsFwQ== + dependencies: + agent-base "^7.0.2" + debug "4" + httpx@^2.2.0, httpx@^2.2.6: version "2.2.7" resolved "https://registry.npmmirror.com/httpx/-/httpx-2.2.7.tgz#1e34198146e32ca3305a66c11209559e1cbeba09" @@ -14818,6 +15840,11 @@ is-interactive@^1.0.0: version "1.0.0" resolved "https://registry.npmmirror.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" +is-invalid-path@^1.0.2: + version "1.0.2" + resolved "https://registry.npmmirror.com/is-invalid-path/-/is-invalid-path-1.0.2.tgz#2f84731559f4936abcf1b227632719cf45c5dc0e" + integrity sha512-6KLcFrPCEP3AFXMfnWrIFkZpYNBVzZAoBJJDEZKtI3LXkaDjM3uFMJQjxiizUuZTZ9Oh9FNv/soXbx5TcpaDmA== + is-lambda@^1.0.1: version "1.0.1" resolved "https://registry.npmmirror.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" @@ -16398,6 +17425,13 @@ jsesc@~0.5.0: version "0.5.0" resolved "https://registry.npmmirror.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json-buffer@3.0.0: version "3.0.0" resolved "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" @@ -16430,7 +17464,7 @@ json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" -json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: +json-stringify-safe@^5.0.0, json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -16488,6 +17522,11 @@ jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.npmmirror.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" +jsonschema@^1.4.1: + version "1.4.1" + resolved "https://registry.npmmirror.com/jsonschema/-/jsonschema-1.4.1.tgz#cc4c3f0077fb4542982973d8a083b6b34f482dab" + integrity sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ== + jsonwebtoken@^8.5.1: version "8.5.1" resolved "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" @@ -17140,6 +18179,17 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" +log4js@^6.9.1: + version "6.9.1" + resolved "https://registry.npmmirror.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.5" + logform@^2.3.2, logform@^2.4.0: version "2.5.1" resolved "https://registry.npmmirror.com/logform/-/logform-2.5.1.tgz#44c77c34becd71b3a42a3970c77929e52c6ed48b" @@ -17155,6 +18205,11 @@ long@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" +long@^5.0.0: + version "5.2.3" + resolved "https://registry.npmmirror.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + longest-streak@^3.0.0: version "3.1.0" resolved "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" @@ -18057,7 +19112,7 @@ minimatch@3.0.4: dependencies: brace-expansion "^1.1.7" -minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@3.1.2, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" dependencies: @@ -18391,7 +19446,7 @@ named-placeholders@^1.1.2: dependencies: lru-cache "^7.14.1" -nan@^2.12.1: +nan@^2.12.1, nan@^2.16.0, nan@^2.17.0: version "2.17.0" resolved "https://registry.npmmirror.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" @@ -18468,6 +19523,30 @@ netmask@^2.0.2: version "2.0.2" resolved "https://registry.npmmirror.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" +newrelic@^10.6.2: + version "10.6.2" + resolved "https://registry.npmmirror.com/newrelic/-/newrelic-10.6.2.tgz#a2f3b18168f92e8e4bffac5f34f3232ca02aa3cb" + integrity sha512-eCne93dEXcXsoFhjFnFJF6AO1PDhMZGA4G8i/sf5YjpaYXyUNfWacsDHQUM+0r1cf/BW8gL8HEVmIIW5uyAaXA== + dependencies: + "@grpc/grpc-js" "^1.8.10" + "@grpc/proto-loader" "^0.7.5" + "@mrleebo/prisma-ast" "^0.5.2" + "@newrelic/aws-sdk" "^6.0.0" + "@newrelic/koa" "^7.1.1" + "@newrelic/security-agent" "0.2.1" + "@newrelic/superagent" "^6.0.0" + "@tyriar/fibonacci-heap" "^2.0.7" + concat-stream "^2.0.0" + https-proxy-agent "^7.0.1" + json-bigint "^1.0.0" + json-stringify-safe "^5.0.0" + readable-stream "^3.6.1" + semver "^7.5.2" + winston-transport "^4.5.0" + optionalDependencies: + "@contrast/fn-inspect" "^3.3.0" + "@newrelic/native-metrics" "^9.0.1" + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.npmmirror.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -18517,6 +19596,11 @@ node-fetch@^3.2.0: fetch-blob "^3.1.4" formdata-polyfill "^4.0.10" +node-gyp-build@^4.4.0: + version "4.6.0" + resolved "https://registry.npmmirror.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" + integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== + node-gyp@8.x: version "8.4.1" resolved "https://registry.npmmirror.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" @@ -19431,6 +20515,11 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5: pbkdf2 "^3.0.3" safe-buffer "^5.1.1" +parse-cache-control@^1.0.1: + version "1.0.1" + resolved "https://registry.npmmirror.com/parse-cache-control/-/parse-cache-control-1.0.1.tgz#8eeab3e54fa56920fe16ba38f77fa21aacc2d74e" + integrity sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg== + parse-entities@^2.0.0: version "2.0.0" resolved "https://registry.npmmirror.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" @@ -20798,6 +21887,11 @@ prettier@^3.0.0: version "3.0.0" resolved "https://registry.npmmirror.com/prettier/-/prettier-3.0.0.tgz#e7b19f691245a21d618c68bc54dc06122f6105ae" +pretty-bytes@^5.6.0: + version "5.6.0" + resolved "https://registry.npmmirror.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + pretty-error@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" @@ -20907,6 +22001,13 @@ promise.series@^0.2.0: version "0.2.0" resolved "https://registry.npmmirror.com/promise.series/-/promise.series-0.2.0.tgz#2cc7ebe959fc3a6619c04ab4dbdc9e452d864bbd" +promise@^8.0.0: + version "8.3.0" + resolved "https://registry.npmmirror.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a" + integrity sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg== + dependencies: + asap "~2.0.6" + promise@~7.0.1: version "7.0.4" resolved "https://registry.npmmirror.com/promise/-/promise-7.0.4.tgz#363e84a4c36c8356b890fed62c91ce85d02ed539" @@ -20956,6 +22057,24 @@ proto-list@~1.2.1: version "1.2.4" resolved "https://registry.npmmirror.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" +protobufjs@^7.2.4: + version "7.2.5" + resolved "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.2.5.tgz#45d5c57387a6d29a17aab6846dcc283f9b8e7f2d" + integrity sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + protocols@^1.4.0: version "1.4.8" resolved "https://registry.npmmirror.com/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8" @@ -21957,7 +23076,7 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.1: version "3.6.2" resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" dependencies: @@ -22089,6 +23208,11 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" +regexp-to-ast@0.5.0: + version "0.5.0" + resolved "https://registry.npmmirror.com/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz#56c73856bee5e1fef7f73a00f1473452ab712a24" + integrity sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw== + regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.0: version "1.5.0" resolved "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" @@ -22288,6 +23412,11 @@ replace-ext@^2.0.0: version "2.0.0" resolved "https://registry.npmmirror.com/replace-ext/-/replace-ext-2.0.0.tgz#9471c213d22e1bcc26717cd6e50881d88f812b06" +request-ip@^3.3.0: + version "3.3.0" + resolved "https://registry.npmmirror.com/request-ip/-/request-ip-3.3.0.tgz#863451e8fec03847d44f223e30a5d63e369fa611" + integrity sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA== + request-promise-core@1.1.4: version "1.1.4" resolved "https://registry.npmmirror.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" @@ -22531,6 +23660,11 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +ringbufferjs@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/ringbufferjs/-/ringbufferjs-2.0.0.tgz#09f40e2675a99cfef430b7ec5815ac1bc2e24120" + integrity sha512-GCOqTzUsTHF7nrqcgtNGAFotXztLgiePpIDpyWZ7R5I02tmfJWV+/yuJc//Hlsd8G+WzI1t/dc2y/w2imDZdog== + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.npmmirror.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -22917,7 +24051,7 @@ semver@^7.1.1, semver@^7.1.3, semver@^7.2, semver@^7.3.2, semver@^7.3.4, semver@ dependencies: lru-cache "^6.0.0" -semver@^7.5.3, semver@^7.5.4: +semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.npmmirror.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" dependencies: @@ -22975,6 +24109,20 @@ serve-handler@6.1.3: path-to-regexp "2.2.1" range-parser "1.2.0" +serve-handler@^6.1.5: + version "6.1.5" + resolved "https://registry.npmmirror.com/serve-handler/-/serve-handler-6.1.5.tgz#a4a0964f5c55c7e37a02a633232b6f0d6f068375" + integrity sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg== + dependencies: + bytes "3.0.0" + content-disposition "0.5.2" + fast-url-parser "1.1.3" + mime-types "2.1.18" + minimatch "3.1.2" + path-is-inside "1.0.2" + path-to-regexp "2.2.1" + range-parser "1.2.0" + serve@^13.0.2: version "13.0.4" resolved "https://registry.npmmirror.com/serve/-/serve-13.0.4.tgz#fc4466dc84b3e4a6cb622247c85ed8afe4b88820" @@ -23596,6 +24744,15 @@ stream-wormhole@^1.0.4: version "1.1.0" resolved "https://registry.npmmirror.com/stream-wormhole/-/stream-wormhole-1.1.0.tgz#300aff46ced553cfec642a05251885417693c33d" +streamroller@^3.1.5: + version "3.1.5" + resolved "https://registry.npmmirror.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" + integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + fs-extra "^8.1.0" + streamsearch@0.1.2: version "0.1.2" resolved "https://registry.npmmirror.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" @@ -23673,6 +24830,11 @@ string-width@^5.0.0: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string.fromcodepoint@^0.2.1: + version "0.2.1" + resolved "https://registry.npmmirror.com/string.fromcodepoint/-/string.fromcodepoint-0.2.1.tgz#8d978333c0bc92538f50f383e4888f3e5619d653" + integrity sha512-n69H31OnxSGSZyZbgBlvYIXlrMhJQ0dQAX1js1QDhpaUH6zmU3QYlj07bCwCNlPOu3oRXIubGPl2gDGnHsiCqg== + string.prototype.matchall@^4.0.8: version "4.0.8" resolved "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" @@ -24033,6 +25195,22 @@ symbol-tree@^3.2.2, symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" +sync-request@^6.1.0: + version "6.1.0" + resolved "https://registry.npmmirror.com/sync-request/-/sync-request-6.1.0.tgz#e96217565b5e50bbffe179868ba75532fb597e68" + integrity sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw== + dependencies: + http-response-object "^3.0.1" + sync-rpc "^1.2.1" + then-request "^6.0.0" + +sync-rpc@^1.2.1: + version "1.3.6" + resolved "https://registry.npmmirror.com/sync-rpc/-/sync-rpc-1.3.6.tgz#b2e8b2550a12ccbc71df8644810529deb68665a7" + integrity sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw== + dependencies: + get-port "^3.1.0" + synckit@0.8.5, synckit@^0.8.5: version "0.8.5" resolved "https://registry.npmmirror.com/synckit/-/synckit-0.8.5.tgz#b7f4358f9bb559437f9f167eb6bc46b3c9818fa3" @@ -24223,6 +25401,23 @@ textextensions@^2.5.0: version "2.6.0" resolved "https://registry.npmmirror.com/textextensions/-/textextensions-2.6.0.tgz#d7e4ab13fe54e32e08873be40d51b74229b00fc4" +then-request@^6.0.0: + version "6.0.2" + resolved "https://registry.npmmirror.com/then-request/-/then-request-6.0.2.tgz#ec18dd8b5ca43aaee5cb92f7e4c1630e950d4f0c" + integrity sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA== + dependencies: + "@types/concat-stream" "^1.6.0" + "@types/form-data" "0.0.33" + "@types/node" "^8.0.0" + "@types/qs" "^6.2.31" + caseless "~0.12.0" + concat-stream "^1.6.0" + form-data "^2.2.0" + http-basic "^8.1.1" + http-response-object "^3.0.1" + promise "^8.0.0" + qs "^6.4.0" + thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" @@ -24917,6 +26112,13 @@ underscore@^1.13.1: version "1.13.6" resolved "https://registry.npmmirror.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" +unescape-js@^1.1.4: + version "1.1.4" + resolved "https://registry.npmmirror.com/unescape-js/-/unescape-js-1.1.4.tgz#4bc6389c499cb055a98364a0b3094e1c3d5da395" + integrity sha512-42SD8NOQEhdYntEiUQdYq/1V/YHwr1HLwlHuTJB5InVVdOSbgI6xu8jK5q65yIzuFCfczzyDF/7hbGzVbyCw0g== + dependencies: + string.fromcodepoint "^0.2.1" + unescape@^1.0.1: version "1.0.1" resolved "https://registry.npmmirror.com/unescape/-/unescape-1.0.1.tgz#956e430f61cad8a4d57d82c518f5e6cc5d0dda96" @@ -25933,10 +27135,15 @@ ws@^5.2.0: dependencies: async-limiter "~1.0.0" -ws@^7.0.0, ws@^7.4.6: +ws@^7.0.0, ws@^7.4.6, ws@^7.5.9: version "7.5.9" resolved "https://registry.npmmirror.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" +ws@^8.13.0: + version "8.13.0" + resolved "https://registry.npmmirror.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" + integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + ws@~7.4.0: version "7.4.6" resolved "https://registry.npmmirror.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" @@ -26140,7 +27347,7 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.0, yargs@^17.3.1, yargs@^17.5.1: +yargs@^17.0.0, yargs@^17.3.1, yargs@^17.5.1, yargs@^17.7.2: version "17.7.2" resolved "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" dependencies: