commit f3b09bbed43174e1827c11ee6cb163d3f483a484 Author: yixr <1> Date: Wed Oct 16 11:30:51 2024 +0800 本地oa-backup diff --git a/.cz-config.js b/.cz-config.js new file mode 100644 index 0000000..57d4806 --- /dev/null +++ b/.cz-config.js @@ -0,0 +1,94 @@ +// 请使用npm run c提交代码。遵循代码提交规范 +module.exports = { + types: [ + { value: 'feat', name: '功能: ✨ 新增功能', emoji: ':sparkles:' }, + { value: 'fix', name: '修复: 🐛 修复缺陷', emoji: ':bug:' }, + { value: 'docs', name: '文档: 📝 文档变更', emoji: ':memo:' }, + { + value: 'style', + name: '格式: 🌈 代码格式(不影响功能,例如空格、分号等格式修正)', + emoji: ':lipstick:', + }, + { + value: 'refactor', + name: '重构: 🔄 代码重构(不包括 bug 修复、功能新增)', + emoji: ':recycle:', + }, + { value: 'perf', name: '性能: 🚀 性能优化', emoji: ':zap:' }, + { + value: 'test', + name: '测试: 🧪 添加疏漏测试或已有测试改动', + emoji: ':white_check_mark:', + }, + { + value: 'build', + name: '构建: 📦️ 构建流程、外部依赖变更(如升级 npm 包、修改 vite 配置等)', + emoji: ':package:', + }, + { + value: 'ci', + name: '集成: ⚙️ 修改 CI 配置、脚本', + emoji: ':ferris_wheel:', + }, + { value: 'revert', name: '回退: ↩️ 回滚 commit', emoji: ':rewind:' }, + { + value: 'chore', + name: '其他: 🛠️ 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)', + emoji: ':hammer:', + }, + ], + useEmoji: true, + emojiAlign: 'center', + useAI: false, + aiNumber: 1, + themeColorCode: '', + scopes: [], + allowCustomScopes: true, + allowEmptyScopes: true, + customScopesAlign: 'bottom', + customScopesAlias: 'custom', + emptyScopesAlias: 'empty', + upperCaseSubject: false, + markBreakingChangeMode: false, + breaklineNumber: 100, + breaklineChar: '|', + issuePrefixes: [ + { value: 'closed', name: 'closed: ISSUES has been processed' }, + ], + customIssuePrefixAlign: 'top', + emptyIssuePrefixAlias: 'skip', + customIssuePrefixAlias: 'custom', + allowCustomIssuePrefix: true, + allowEmptyIssuePrefix: true, + confirmColorize: true, + maxHeaderLength: Infinity, + maxSubjectLength: Infinity, + minSubjectLength: 0, + scopeOverrides: undefined, + defaultBody: '', + defaultIssues: '', + defaultScope: '', + defaultSubject: '', + messages: { + type: '选择一种你期望的提交类型(type):', + // scope: '选择一个更改的范围(scope) (可选):', + // used if allowCustomScopes is true + // customScope: 'Denote the SCOPE of this change:', + subject: '输入本次commit记录说明:\n', + // body: '长说明,使用"|"换行(可选):\n', + // breaking: '非兼容性说明 (可选):\n', + // footer: '关联关闭的issue,例如:#31, #34(可选):\n', + confirmCommit: '确定提交说明?', + }, + skipQuestions: ['scope', 'body', 'breaking', 'footer'], + allowBreakingChanges: [ + 'fix', + 'feat', + 'update', + 'refactor', + 'perf', + 'build', + 'revert', + ], + subjectLimit: 500, // 提交长度限制500 +}; diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..84cee7b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,46 @@ +# compiled output +/dist +/node_modules +# package-lock.json +# yarn.lock + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# Code +src/config/config.development.* +docs/* +# sql/* +test/* +README.md + +# Dev data +/__data/ \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..b6ff217 --- /dev/null +++ b/.env @@ -0,0 +1,40 @@ +# app +APP_NAME = Huaxin OA +APP_PORT = 8001 +APP_BASE_URL = http://localhost:${APP_PORT} +APP_LOCALE = zh-CN + +# cluster +CPU_LEN = 1 + +# logger +LOGGER_LEVEL = verbose +LOGGER_MAX_FILES = 31 + +TZ = Asia/Shanghai + +# OSS(minio) +OSS_ACCESSKEY=8Zttvx4ZbF2ikFRb +OSS_SECRETKEY=SCgOJEJXM5vMNQL4fF8opXA1wmpACRfw +OSS_PORT=8021 +OSS_DOMAIN=144.123.43.138 +OSS_DOMAIN_USE_SSL=false +OSS_BUCKET=tes1 +OSS_ZONE=Zone_z2 # Zone_as0 | Zone_na0 | Zone_z0 | Zone_z1 | Zone_z2 +OSS_ACCESS_TYPE=public # or private + + + +DB_HOST = host.docker.internal +DB_PORT = 13307 +DB_DATABASE = hxoa +DB_USERNAME = root +DB_PASSWORD = huaxin123 +DB_SYNCHRONIZE = false +DB_LOGGING = ["error"] + +REDIS_PORT = 6379 +REDIS_HOST = host.docker.internal +REDIS_PASSWORD = 123456 +REDIS_DB = 0 + diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..78a78df --- /dev/null +++ b/.env.development @@ -0,0 +1,35 @@ +# logger +LOGGER_LEVEL = debug + +# security +JWT_SECRET = admin!@#123 +JWT_EXPIRE = 86400 # 单位秒 +REFRESH_TOKEN_SECRET = admin!@#123 +REFRESH_TOKEN_EXPIRE = 2592000 + +# swagger +SWAGGER_ENABLE = true +SWAGGER_PATH = api-docs +SWAGGER_VERSION = 1.0 + +# db +DB_HOST = localhost +DB_PORT = 13307 +DB_DATABASE = hxoa +DB_USERNAME = root +DB_PASSWORD = huaxin123 +DB_SYNCHRONIZE = true +DB_LOGGING = "all" + +# redis +REDIS_PORT = 6379 +REDIS_HOST = localhost +REDIS_PASSWORD = 123456 +REDIS_DB = 0 + +# smtp +SMTP_HOST = smtp.163.com +SMTP_PORT = 465 +SMTP_USER = nest_admin@163.com +SMTP_PASS = VIPLLOIPMETTROYU + diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..8dc6113 --- /dev/null +++ b/.env.production @@ -0,0 +1,38 @@ +# logger +LOGGER_LEVEL = debug + +# security +JWT_SECRET = admin!@#123 +JWT_EXPIRE = 86400 # 单位秒 +REFRESH_TOKEN_SECRET = admin!@#123 +REFRESH_TOKEN_EXPIRE = 2592000 + +# swagger +SWAGGER_ENABLE = true +SWAGGER_PATH = api-docs +SWAGGER_VERSION = 1.0 + +# db +DB_HOST = host.docker.internal +DB_PORT = 13307 +DB_DATABASE = hxoa +DB_USERNAME = root +DB_PASSWORD = huaxin123 +DB_SYNCHRONIZE = false +DB_LOGGING = ["error"] + +# redis +REDIS_PORT = 6379 +REDIS_HOST = host.docker.internal +REDIS_PASSWORD = 123456 +REDIS_DB = 0 + +# smtp +SMTP_HOST = smtp.163.com +SMTP_PORT = 465 +SMTP_USER = nest_admin@163.com +SMTP_PASS = VIPLLOIPMETTROYU + +# 是否为演示模式(在演示模式下,会拦截除 GET 方法以外的所有请求) +IS_DEMO = false + diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..890e6f9 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,8 @@ +node_modules +dist/ +config +build/ +.eslintrc.js +package.json +tsconfig**.json +.vscode/ \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..0b84b6b --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,22 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin', 'prettier'], + extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-var-requires': 0, + }, +}; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d4e5bd3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings + +# Automatically normalize line endings (to LF) for all text-based files. +* text=auto eol=lf + +# Declare files that will always have CRLF line endings on checkout. +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf + +# Denote all files that are truly binary and should not be modified. +*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9caffb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +node_modules +.DS_Store +dist +*-dist +.cache +.history +.vercel/ + +.turbo +.local + +# local env files +#.env.development +#.env.production +.env.local + +.eslintcache + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Editor directories and files +.idea +# .vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# compiled output +/dist +/node_modules + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +.nestjs_repl_history +out + +# temp data +__data +# 我想把upload文件夹传上去 +/public/upload/* +!/public/upload/.gitkeep +types/env.d.ts \ No newline at end of file diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..35ed753 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npm run commitlint diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..3bb6316 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +#推送之前运行eslint检查 +npx lint-staged +##推送之前运行单元测试检查 +#npm run test:unit diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..eb950ed --- /dev/null +++ b/.npmrc @@ -0,0 +1,6 @@ +shamefully-hoist=true +strict-peer-dependencies=false + +# 使用淘宝镜像源 +registry = https://registry.npmmirror.com +# registry = https://registry.npmjs.org \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..a68494c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +/dist/* +.local +.output.js +/node_modules/** + +**/*.svg +**/*.sh + +/public/* +test/**/* +/.vscode/* \ No newline at end of file diff --git a/.prettierrc.cjs b/.prettierrc.cjs new file mode 100644 index 0000000..552b0a7 --- /dev/null +++ b/.prettierrc.cjs @@ -0,0 +1,11 @@ +module.exports = { + printWidth: 100, // 每行代码长度(默认80) + tabWidth: 2, // 每个tab相当于多少个空格(默认2) + useTabs: false, // 是否使用tab进行缩进(默认false) + singleQuote: true, // 使用单引号(默认false) + semi: true, // 声明结尾使用分号(默认true) + trailingComma: 'none', // 多行使用拖尾逗号(默认none) + bracketSpacing: true, // 对象字面量的大括号间使用空格(默认true) + arrowParens: 'avoid', // 只有一个参数的箭头函数的参数是否带圆括号(默认avoid) + endOfLine: 'auto', // 文件换行格式 LF/CRLF +}; diff --git a/.versionrc.js b/.versionrc.js new file mode 100644 index 0000000..e4ad6c6 --- /dev/null +++ b/.versionrc.js @@ -0,0 +1,19 @@ +//发布应用 生成commit日志记录。 + +module.exports = { + types: [ + { type: 'feat', section: '✨ Features | 新功能' }, + { type: 'fix', section: '🐛 Bug Fixes | Bug 修复' }, + { type: 'init', section: '📦️ Init | 初始化' }, + { type: 'docs', section: '📝 Documentation | 文档' }, + { type: 'style', section: '🌈 Styles | 风格' }, + { type: 'refactor', section: '🔄 Code Refactoring | 代码重构' }, + { type: 'perf', section: '🚀 Performance Improvements | 性能优化' }, + { type: 'test', section: '🧪 Tests | 测试' }, + { type: 'revert', section: '↩️ Revert | 回退' }, + { type: 'build', section: '📦️ Build System | 打包构建' }, + { type: 'update', section: '⚙️ update | 构建/工程依赖/工具升级' }, + { type: 'tool', section: '🛠️ tool | 工具升级' }, + { type: 'ci', section: '⚙️ Continuous Integration | CI 配置' }, + ], +}; diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d879beb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Nest Framework", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"], + "autoAttachChildProcesses": true, + "restart": true, + "sourceMaps": true, + "stopOnEntry": false, + "console": "integratedTerminal" + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fa2b9dd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# 遇到网络问题可以配置镜像加速:https://gist.github.com/y0ngb1n/7e8f16af3242c7815e7ca2f0833d3ea6 +# FROM 表示设置要制作的镜像基于哪个镜像,FROM指令必须是整个Dockerfile的第一个指令,如果指定的镜像不存在默认会自动从Docker Hub上下载。 +# 指定我们的基础镜像是node,latest表示版本是最新, 如果要求空间极致,可以选择lts-alpine +# 使用 as 来为某一阶段命名 +FROM node:20-slim AS base + +ENV PROJECT_DIR=/huaxin-admin \ + DB_HOST=mysql \ + APP_PORT=8001 \ + PNPM_HOME="/pnpm" \ + PATH="$PNPM_HOME:$PATH" + + +RUN corepack enable \ + && yarn global add pm2 + +# WORKDIR指令用于设置Dockerfile中的RUN、CMD和ENTRYPOINT指令执行命令的工作目录(默认为/目录),该指令在Dockerfile文件中可以出现多次, +# 如果使用相对路径则为相对于WORKDIR上一次的值, +# 例如WORKDIR /data,WORKDIR logs,RUN pwd最终输出的当前目录是/data/logs。 +# cd 到 /huaxin-admin +WORKDIR $PROJECT_DIR +COPY ./ $PROJECT_DIR +RUN chmod +x ./wait-for-it.sh + +# set timezone +RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ + && echo 'Asia/Shanghai' > /etc/timezone + +# see https://pnpm.io/docker +FROM base AS prod-deps +RUN pnpm config set registry https://registry.npmmirror.com +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile + +FROM base AS build +RUN pnpm config set registry https://registry.npmmirror.com +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile +RUN pnpm run build + + +# mirror acceleration +# RUN npm config set registry https://registry.npmmirror.com + +# RUN npm config rm proxy && npm config rm https-proxy + +FROM base +COPY --from=prod-deps $PROJECT_DIR/node_modules $PROJECT_DIR/node_modules +COPY --from=build $PROJECT_DIR/dist $PROJECT_DIR/dist + +# EXPOSE port +EXPOSE $APP_PORT + +# 容器启动时执行的命令,类似npm run start +# CMD ["pnpm", "start:prod"] +# CMD ["pm2-runtime", "ecosystem.config.js"] +ENTRYPOINT ./wait-for-it.sh $DB_HOST:$DB_PORT -- pm2-runtime ecosystem.config.js diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ea7c2f9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024-present Louis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f08f061 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ + +## 环境要求 + +- `nodejs` `16.20.2`+ +- `docker` `20.x`+ ,其中 `docker compose`版本需要 `2.17.0`+ +- `mysql` `8.x`+ +- 使用 [`pnpm`](https://pnpm.io/zh/) 包管理器安装项目依赖 + + +| 账号 | 密码 | 权限 | +| :-------: | :----: | :--------: | +| admin | a123456 | 超级管理员 | + + +## 本地开发 + +- 【可选】如果你是新手,还不太会搭建`mysql/redis`,你可以使用 `Docker` 启动指定服务供本地开发时使用, 例如: + +```bash +# 启动MySql服务 +docker compose --env-file .env --env-file .env.development run -d --service-ports mysql +# 启动Redis服务 +docker compose --env-file .env --env-file .env.development run -d --service-ports redis +``` + +- 安装依赖 + +```bash + +pnpm install + +``` + +- 运行 + 启动成功后,通过 访问。 + +```bash +pnpm dev +``` + +- 打包 + +```bash +pnpm build +``` + +2.使用docker运行 + +```bash +docker compose up -d +``` + +停止并删除所有容器 + +```bash +pnpm docker:down +# or +docker compose --env-file .env --env-file .env.production down +``` + +删除镜像 + +```bash +pnpm docker:rmi +# or +docker rmi huaxin-admin-server:stable +``` + +查看实时日志输出 + +```bash +pnpm docker:logs +# or +docker-compose --env-file .env --env-file .env.production logs -f + +``` + +## 数据库迁移 + +1. 更新数据库(或初始化数据) + +```bash +pnpm migration:run +``` + +2. 生成迁移 + +```bash +pnpm migration:generate +``` + +3. 回滚到最后一次更新 + +```bash +pnpm migration:revert +``` +4.执行sql覆盖docker中的数据库 + +```bash +docker exec -i huaxin-admin-mysql mysql -h 127.0.0.1 -u root -phuaxin123 hxoa < huaxinoa0327.sql +``` + +更多细节,请移步至[官方文档](https://typeorm.io/migrations) + +> [!TIP] +> 如果你的`实体类`或`数据库配置`有更新,请执行`npm run build`后再进行数据库迁移相关操作。 + +### 部署 +chmod +x deploy.sh +./deploy.sh \ No newline at end of file diff --git a/commitlint.config.cjs b/commitlint.config.cjs new file mode 100644 index 0000000..7576451 --- /dev/null +++ b/commitlint.config.cjs @@ -0,0 +1,24 @@ +// 请使用npm run c/yarn c提交代码。 +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [ + 2, + 'always', + [ + 'feat', // 新增功能 + 'fix', // 修复缺陷 + 'docs', // 文档变更 + 'style', // 代码格式(不影响功能,例如空格、分号等格式修正) + 'refactor', // 代码重构(不包括 bug 修复、功能新增) + 'perf', // 性能优化 + 'test', // 添加疏漏测试或已有测试改动 + 'build', // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等) + 'ci', // 修改 CI 配置、脚本 + 'revert', // 回滚 commit + 'chore', // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例) + ], + ], + 'subject-case': [0], // subject大小写不做校验 + }, +}; diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..271ef35 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# 拉取最新的代码 + +git config core.fileMode false + +git pull + +docker-compose --env-file .env --env-file .env.production up -d --build \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d2404db --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,68 @@ +version: '3' + +services: + mysql: + image: mysql:8.0 + container_name: huaxin-admin-mysql + restart: always + env_file: + - .env + - .env.production + environment: + - MYSQL_HOST=${DB_HOST} + - MYSQL_PORT=${DB_PORT} + - MYSQL_DATABASE=${DB_DATABASE} + - MYSQL_USERNAME=${DB_USERNAME} + - MYSQL_PASSWORD=${DB_PASSWORD} + - MYSQL_ROOT_PASSWORD=${DB_PASSWORD} + ports: + - '${DB_PORT}:3306' + command: + mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci + volumes: + - ./__data/mysql/:/var/lib/mysql/ # ./__data/mysql/ 路径可以替换成自己的路径 + - ./init_data/sql/:/docker-entrypoint-initdb.d/ # 初始化的脚本,若 ./__data/mysql/ 文件夹存在数据,则不会执行初始化脚本 + networks: + - huaxin_admin_net + + redis: + image: redis:alpine + container_name: huaxin-admin-redis + restart: always + env_file: + - .env + - .env.production + ports: + - '${REDIS_PORT}:6379' + command: > + --requirepass ${REDIS_PASSWORD} + networks: + - huaxin_admin_net + + huaxin-admin-server: + # build: 从当前路径构建镜像 + build: + context: . + dockerfile: Dockerfile + container_name: huaxin-admin-server + restart: always + env_file: + - .env + - .env.production + extra_hosts: + - 'host.docker.internal:host-gateway' + ports: + - '${APP_PORT}:${APP_PORT}' + volumes: + # 将容器中huaxin-admin/dist文件夹映射到本地的dist文件夹 + - ./public:/huaxin-admin/public + # 当前服务启动之前先要把depends_on指定的服务启动起来才行 + depends_on: + - mysql + - redis + networks: + - huaxin_admin_net + +networks: + huaxin_admin_net: + name: huaxin_admin_net diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..b08faf1 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,22 @@ +const { cpus } = require('os'); + +const cpuLen = cpus().length; + +module.exports = { + apps: [ + { + name: 'huaxin-admin', + script: './dist/main.js', + autorestart: true, + exec_mode: 'cluster', + watch: false, + instances: process.env.CPU_LEN ?? cpuLen, + max_memory_restart: '520M', + args: '', + env: { + NODE_ENV: 'production', + PORT: process.env.APP_PORT + } + } + ] +}; diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..c3ca7c3 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,36 @@ +const antfu = require('@antfu/eslint-config').default + +module.exports = antfu({ + stylistic: { + indent: 2, + quotes: 'single', + }, + typescript: true, +}, { + rules: { + 'no-console': 'off', + 'unused-imports/no-unused-vars': 'off', + 'unused-imports/no-unused-imports': 2, + + 'ts/consistent-type-imports': 'off', + 'node/prefer-global/process': 'off', + 'node/prefer-global/buffer': 'off', + + 'import/order': [ + 2, + { + 'pathGroups': [ + { + pattern: '~/**', + group: 'external', + position: 'after', + }, + ], + 'alphabetize': { order: 'asc', caseInsensitive: false }, + 'newlines-between': 'always-and-inside-groups', + 'warnOnUnassignedImports': true, + }, + ], + + }, +}) diff --git a/init_data/sql/hxoa.sql b/init_data/sql/hxoa.sql new file mode 100644 index 0000000..1edf3cd --- /dev/null +++ b/init_data/sql/hxoa.sql @@ -0,0 +1,1090 @@ +/* + Navicat Premium Data Transfer + + Source Server : localhost + Source Server Type : MySQL + Source Server Version : 80036 + Source Host : localhost:13307 + Source Schema : hxoa + + Target Server Type : MySQL + Target Server Version : 80036 + File Encoding : 65001 + + Date: 07/04/2024 11:11:06 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for company +-- ---------------------------- +DROP TABLE IF EXISTS `company`; +CREATE TABLE `company` ( + `id` int NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '公司名称', + `is_delete` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `IDX_a76c5cd486f7779bd9c319afd2`(`name`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 19 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of company +-- ---------------------------- +INSERT INTO `company` VALUES (1, '2024-03-04 15:44:43.005593', '2024-03-08 08:48:37.000000', '深圳市立创电子商务有限公司', 0); +INSERT INTO `company` VALUES (4, '2024-03-04 16:05:34.701780', '2024-04-07 09:54:56.000000', '深圳市诚亨泰科技有限公司', 0); +INSERT INTO `company` VALUES (5, '2024-03-04 16:05:38.867786', '2024-03-04 16:05:38.867786', '东莞市顶源电子有限公司', 0); +INSERT INTO `company` VALUES (6, '2024-03-04 16:05:42.479027', '2024-03-04 16:05:42.479027', '深圳市福田区赛格电子市场金佳电子经营部', 0); +INSERT INTO `company` VALUES (7, '2024-03-04 16:05:46.775364', '2024-03-04 16:05:46.775364', '深圳市思界电子科技有限公司', 0); +INSERT INTO `company` VALUES (8, '2024-03-04 16:05:55.806537', '2024-03-04 16:05:55.806537', '广州市星翼电信科技有限公司', 0); +INSERT INTO `company` VALUES (9, '2024-03-04 16:06:03.003860', '2024-03-04 16:09:49.000000', '快递费', 1); +INSERT INTO `company` VALUES (10, '2024-03-04 16:06:09.788572', '2024-03-04 16:06:09.788572', '青岛丰喆精密模具有限公司', 0); +INSERT INTO `company` VALUES (11, '2024-03-04 16:06:12.872983', '2024-03-04 16:06:12.872983', '深圳嘉立创科技集团股份有限公司', 0); +INSERT INTO `company` VALUES (12, '2024-03-04 16:06:19.823410', '2024-03-04 16:06:19.823410', '北京特倍福电子技术有限公司', 0); +INSERT INTO `company` VALUES (13, '2024-03-04 16:06:25.937749', '2024-03-04 16:06:25.937749', '上海脉芯网络科技有限公司', 0); +INSERT INTO `company` VALUES (14, '2024-03-22 11:01:20.588144', '2024-03-22 11:01:20.588144', '深圳市声能达科技有限公司', 0); +INSERT INTO `company` VALUES (15, '2024-03-26 10:29:45.595059', '2024-03-26 10:29:45.595059', '深圳市新得润电子有限公司', 0); +INSERT INTO `company` VALUES (16, '2024-04-05 08:47:49.227114', '2024-04-05 08:47:49.227114', '山东矿机华信智能科技有限公司', 0); +INSERT INTO `company` VALUES (17, '2024-04-05 10:03:44.190698', '2024-04-05 10:03:44.190698', '广东润宇传感器股份有限公司', 0); +INSERT INTO `company` VALUES (18, '2024-04-05 15:21:21.989529', '2024-04-05 15:21:21.989529', '宝鸡兴宇腾测控设备有限公司', 0); + +-- ---------------------------- +-- Table structure for company_storage +-- ---------------------------- +DROP TABLE IF EXISTS `company_storage`; +CREATE TABLE `company_storage` ( + `company_id` int NOT NULL, + `file_id` int NOT NULL, + PRIMARY KEY (`company_id`, `file_id`) USING BTREE, + INDEX `IDX_0958ee6ca6f52985840624bb91`(`company_id`) USING BTREE, + INDEX `IDX_bdd3a301229b9dec4b95549dfe`(`file_id`) USING BTREE, + CONSTRAINT `FK_0958ee6ca6f52985840624bb916` FOREIGN KEY (`company_id`) REFERENCES `company` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `FK_bdd3a301229b9dec4b95549dfe7` FOREIGN KEY (`file_id`) REFERENCES `tool_storage` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of company_storage +-- ---------------------------- + +-- ---------------------------- +-- Table structure for contract +-- ---------------------------- +DROP TABLE IF EXISTS `contract`; +CREATE TABLE `contract` ( + `id` int NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `contract_number` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '合同编号', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '合同标题', + `type` int NOT NULL COMMENT '合同类型(字典)', + `party_a` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '甲方', + `party_b` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '乙方', + `signing_date` date NULL DEFAULT NULL, + `delivery_deadline` date NULL DEFAULT NULL, + `status` tinyint NOT NULL DEFAULT 0 COMMENT '审核状态(字典)', + `is_delete` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `IDX_a2f8461960ce0fcbd0d6551009`(`contract_number`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of contract +-- ---------------------------- + +-- ---------------------------- +-- Table structure for contract_storage +-- ---------------------------- +DROP TABLE IF EXISTS `contract_storage`; +CREATE TABLE `contract_storage` ( + `contract_id` int NOT NULL, + `file_id` int NOT NULL, + PRIMARY KEY (`contract_id`, `file_id`) USING BTREE, + INDEX `IDX_b0a3f22af56decbc128c674447`(`contract_id`) USING BTREE, + INDEX `IDX_2fe7cda0f292b099b7e13f8f61`(`file_id`) USING BTREE, + CONSTRAINT `FK_2fe7cda0f292b099b7e13f8f612` FOREIGN KEY (`file_id`) REFERENCES `tool_storage` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `FK_b0a3f22af56decbc128c674447e` FOREIGN KEY (`contract_id`) REFERENCES `contract` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of contract_storage +-- ---------------------------- + +-- ---------------------------- +-- Table structure for materials_in_out +-- ---------------------------- +DROP TABLE IF EXISTS `materials_in_out`; +CREATE TABLE `materials_in_out` ( + `id` int NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `product_id` int NOT NULL COMMENT '产品', + `in_or_out` tinyint NOT NULL COMMENT '入库或出库', + `quantity` int NOT NULL DEFAULT 0 COMMENT '数量', + `unit_price` decimal(15, 10) NOT NULL DEFAULT 0.0000000000 COMMENT '单价', + `amount` decimal(15, 10) NOT NULL DEFAULT 0.0000000000 COMMENT '金额', + `agent` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '经办人', + `issuance_number` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '领料单号', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `is_delete` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除', + `inventory_inout_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '原材料出入库编号', + `project_id` int NULL DEFAULT NULL COMMENT '项目', + `inventory_id` int NOT NULL COMMENT '库存', + `time` datetime NULL DEFAULT NULL COMMENT '时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `FK_770f1c4afd9631499ccc08bd58b`(`product_id`) USING BTREE, + INDEX `FK_7a5bd19f8fd458f6336efedf765`(`project_id`) USING BTREE, + INDEX `FK_f5dc1f1e4db2f990ef89f0398fa`(`inventory_id`) USING BTREE, + CONSTRAINT `FK_770f1c4afd9631499ccc08bd58b` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `FK_7a5bd19f8fd458f6336efedf765` FOREIGN KEY (`project_id`) REFERENCES `project` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `FK_f5dc1f1e4db2f990ef89f0398fa` FOREIGN KEY (`inventory_id`) REFERENCES `materials_inventory` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 196 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of materials_in_out +-- ---------------------------- +INSERT INTO `materials_in_out` VALUES (190, '2024-04-05 09:13:45.815541', '2024-04-05 09:13:45.000000', 36, 0, 100, 0.0000000000, 0.0000000000, '孟菲', NULL, '', 0, 'SI1017', 13, 68, NULL); +INSERT INTO `materials_in_out` VALUES (191, '2024-04-05 09:19:39.482730', '2024-04-05 09:19:39.000000', 35, 0, 100, 0.0000000000, 0.0000000000, '孟菲', NULL, '', 0, 'SI1018', 13, 69, NULL); +INSERT INTO `materials_in_out` VALUES (192, '2024-04-05 10:22:32.422926', '2024-04-05 10:22:32.000000', 37, 0, 83, 557.5200000000, 46274.1600000000, '孟菲', NULL, '', 0, 'SI1019', 13, 70, NULL); +INSERT INTO `materials_in_out` VALUES (193, '2024-04-05 14:58:36.121528', '2024-04-05 14:58:48.000000', 36, 0, 300, 0.0000000000, 0.0000000000, '王兴昊', NULL, NULL, 0, 'SI1020', 13, 68, NULL); +INSERT INTO `materials_in_out` VALUES (194, '2024-04-05 15:00:20.218109', '2024-04-05 15:00:20.218109', 35, 0, 300, 0.0000000000, 0.0000000000, '王兴昊', NULL, NULL, 0, 'SI1021', 13, 69, NULL); +INSERT INTO `materials_in_out` VALUES (195, '2024-04-05 15:26:13.804510', '2024-04-05 15:26:13.000000', 38, 0, 130, 0.0000000000, 0.0000000000, '王兴昊', NULL, '', 0, 'SI1022', 13, 71, NULL); + +-- ---------------------------- +-- Table structure for materials_in_out_storage +-- ---------------------------- +DROP TABLE IF EXISTS `materials_in_out_storage`; +CREATE TABLE `materials_in_out_storage` ( + `materials_in_out_id` int NOT NULL, + `file_id` int NOT NULL, + PRIMARY KEY (`materials_in_out_id`, `file_id`) USING BTREE, + INDEX `IDX_9df13ab4d4747575c310668581`(`materials_in_out_id`) USING BTREE, + INDEX `IDX_96c00bfbcd71e93a6cc070e8e6`(`file_id`) USING BTREE, + CONSTRAINT `FK_96c00bfbcd71e93a6cc070e8e6c` FOREIGN KEY (`file_id`) REFERENCES `tool_storage` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `FK_9df13ab4d4747575c3106685810` FOREIGN KEY (`materials_in_out_id`) REFERENCES `materials_in_out` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of materials_in_out_storage +-- ---------------------------- + +-- ---------------------------- +-- Table structure for materials_inventory +-- ---------------------------- +DROP TABLE IF EXISTS `materials_inventory`; +CREATE TABLE `materials_inventory` ( + `id` int NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `is_delete` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除', + `product_id` int NOT NULL COMMENT '产品', + `quantity` int NOT NULL DEFAULT 0 COMMENT '库存产品数量', + `unit_price` decimal(15, 10) NOT NULL DEFAULT 0.0000000000 COMMENT '库存产品单价', + `project_id` int NOT NULL COMMENT '项目', + `position` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '库存位置', + `inventory_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '库存编号', + PRIMARY KEY (`id`) USING BTREE, + INDEX `FK_3915e159f03408035a62721d5be`(`project_id`) USING BTREE, + INDEX `FK_413008d6a91b215b66971c9a9e8`(`product_id`) USING BTREE, + CONSTRAINT `FK_3915e159f03408035a62721d5be` FOREIGN KEY (`project_id`) REFERENCES `project` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `FK_413008d6a91b215b66971c9a9e8` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 72 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of materials_inventory +-- ---------------------------- +INSERT INTO `materials_inventory` VALUES (68, '2024-04-05 09:13:45.802134', '2024-04-05 14:58:36.000000', NULL, 0, 36, 400, 0.0000000000, 13, '库房一, 第三排, 第二层', 'S1014'); +INSERT INTO `materials_inventory` VALUES (69, '2024-04-05 09:19:39.479124', '2024-04-05 15:00:20.000000', NULL, 0, 35, 400, 0.0000000000, 13, '库房一, 第三排, 第二层', 'S1015'); +INSERT INTO `materials_inventory` VALUES (70, '2024-04-05 10:22:32.418802', '2024-04-05 10:22:32.418802', NULL, 0, 37, 83, 557.5200000000, 13, NULL, 'S1016'); +INSERT INTO `materials_inventory` VALUES (71, '2024-04-05 15:26:13.800776', '2024-04-05 15:26:13.800776', NULL, 0, 38, 130, 0.0000000000, 13, NULL, 'S1017'); + +-- ---------------------------- +-- Table structure for product +-- ---------------------------- +DROP TABLE IF EXISTS `product`; +CREATE TABLE `product` ( + `id` int NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '产品名称', + `is_delete` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除', + `company_id` int NULL DEFAULT NULL COMMENT '所属公司', + `name_pinyin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '产品名称的拼音', + `unit_id` int NULL DEFAULT NULL COMMENT '单位(字典)', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `product_specification` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '产品规格', + `product_number` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '产品编号', + PRIMARY KEY (`id`) USING BTREE, + INDEX `FK_a0503db1630a5b8a4d7deabd556`(`company_id`) USING BTREE, + INDEX `FK_b15422982adca3bf53adfb535de`(`unit_id`) USING BTREE, + CONSTRAINT `FK_a0503db1630a5b8a4d7deabd556` FOREIGN KEY (`company_id`) REFERENCES `company` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `FK_b15422982adca3bf53adfb535de` FOREIGN KEY (`unit_id`) REFERENCES `sys_dict_item` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 39 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of product +-- ---------------------------- +INSERT INTO `product` VALUES (34, '2024-04-04 14:11:01.924609', '2024-04-04 14:11:01.924609', '网络型控制器按键保护板', 0, 1, 'wangluoxingkongzhiqianjianbaohuban', 15, '网络型控制器保护板', '283*15*4', 'P1027'); +INSERT INTO `product` VALUES (35, '2024-04-05 08:49:29.136458', '2024-04-05 08:49:29.136458', '急停基座', 0, 16, 'jitingjizuo', 15, 'ERP库存名急停端子', 'ZB2BZ102C', 'P1028'); +INSERT INTO `product` VALUES (36, '2024-04-05 08:57:45.214384', '2024-04-05 08:57:45.214384', '急停按钮', 0, 16, 'jitinganniu', 15, NULL, 'ZB2BT4C', 'P1029'); +INSERT INTO `product` VALUES (37, '2024-04-05 10:13:36.340912', '2024-04-05 10:13:36.340912', '传感器', 0, 17, 'chuanganqi', 14, NULL, 'GUC960-700mm', 'P1030'); +INSERT INTO `product` VALUES (38, '2024-04-05 15:21:41.040381', '2024-04-05 15:21:41.040381', '压力传感器', 0, 18, 'yalichuanganqi', 14, NULL, 'GPD60', 'P1031'); + +-- ---------------------------- +-- Table structure for product_storage +-- ---------------------------- +DROP TABLE IF EXISTS `product_storage`; +CREATE TABLE `product_storage` ( + `product_id` int NOT NULL, + `file_id` int NOT NULL, + PRIMARY KEY (`product_id`, `file_id`) USING BTREE, + INDEX `IDX_6dd288598f0a0ea3f72f31cb42`(`product_id`) USING BTREE, + INDEX `IDX_eecbd68d7d4d565baecee2d76c`(`file_id`) USING BTREE, + CONSTRAINT `FK_6dd288598f0a0ea3f72f31cb422` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `FK_eecbd68d7d4d565baecee2d76c7` FOREIGN KEY (`file_id`) REFERENCES `tool_storage` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of product_storage +-- ---------------------------- + +-- ---------------------------- +-- Table structure for project +-- ---------------------------- +DROP TABLE IF EXISTS `project`; +CREATE TABLE `project` ( + `id` int NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '项目名称', + `is_delete` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `IDX_dedfea394088ed136ddadeee89`(`name`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of project +-- ---------------------------- +INSERT INTO `project` VALUES (1, '2024-03-07 09:35:15.276345', '2024-03-08 08:48:21.000000', '星火项目', 0); +INSERT INTO `project` VALUES (2, '2024-03-07 09:35:20.004729', '2024-03-07 09:35:20.004729', '东大项目', 0); +INSERT INTO `project` VALUES (3, '2024-03-07 09:35:29.213057', '2024-03-07 09:35:29.213057', '沙湾煤业项目', 0); +INSERT INTO `project` VALUES (13, '2024-04-02 13:53:11.973237', '2024-04-04 14:15:15.000000', '未分类项目', 0); +INSERT INTO `project` VALUES (14, '2024-04-04 14:13:50.885496', '2024-04-04 14:13:50.885496', '七台河煤矿', 0); +INSERT INTO `project` VALUES (15, '2024-04-04 14:14:02.655982', '2024-04-04 14:14:02.655982', '红旗煤矿', 0); +INSERT INTO `project` VALUES (16, '2024-04-04 14:14:19.407108', '2024-04-04 14:14:19.407108', '首旺煤矿', 0); +INSERT INTO `project` VALUES (17, '2024-04-04 14:14:43.054552', '2024-04-04 14:14:43.054552', '沫凤项目', 0); + +-- ---------------------------- +-- Table structure for project_storage +-- ---------------------------- +DROP TABLE IF EXISTS `project_storage`; +CREATE TABLE `project_storage` ( + `project_id` int NOT NULL, + `file_id` int NOT NULL, + PRIMARY KEY (`project_id`, `file_id`) USING BTREE, + INDEX `IDX_9058e954f8f09e2cfa2261c1f2`(`project_id`) USING BTREE, + INDEX `IDX_ac08ac8e4f973873f03dafaca2`(`file_id`) USING BTREE, + CONSTRAINT `FK_9058e954f8f09e2cfa2261c1f26` FOREIGN KEY (`project_id`) REFERENCES `project` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `FK_ac08ac8e4f973873f03dafaca2b` FOREIGN KEY (`file_id`) REFERENCES `tool_storage` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of project_storage +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sys_captcha_log +-- ---------------------------- +DROP TABLE IF EXISTS `sys_captcha_log`; +CREATE TABLE `sys_captcha_log` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NULL DEFAULT NULL, + `account` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL, + `code` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL, + `provider` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of sys_captcha_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sys_config +-- ---------------------------- +DROP TABLE IF EXISTS `sys_config`; +CREATE TABLE `sys_config` ( + `id` int NOT NULL AUTO_INCREMENT, + `key` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `IDX_2c363c25cf99bcaab3a7f389ba`(`key`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of sys_config +-- ---------------------------- +INSERT INTO `sys_config` VALUES (1, 'sys_user_initPassword', '初始密码', '123456', '创建管理员账号的初始密码', '2023-11-10 00:31:44.154921', '2023-11-10 00:31:44.161263'); +INSERT INTO `sys_config` VALUES (2, 'sys_api_token', 'API Token', 'huaxin-admin', '用于请求 @ApiToken 的控制器', '2023-11-10 00:31:44.154921', '2024-01-29 09:52:27.000000'); +INSERT INTO `sys_config` VALUES (3, 'companyName', '公司名称', '华信智能', '菜单侧栏公司的名称', '2024-03-06 13:06:47.347660', '2024-03-06 13:07:18.000000'); +INSERT INTO `sys_config` VALUES (4, 'inventory_inout_number_prefix_in', '人库单号前缀', 'SI', '人库单号前缀', '2024-03-06 14:50:04.844992', '2024-03-25 15:52:28.000000'); +INSERT INTO `sys_config` VALUES (5, 'inventory_inout_number_prefix_out', '出库单号前缀', 'SO', '出库单号前缀', '2024-03-22 13:37:21.165879', '2024-03-25 15:52:32.000000'); +INSERT INTO `sys_config` VALUES (6, 'product_number_prefix', '产品编号前缀', 'P', '产品编号前缀', '2024-03-22 15:51:10.960064', '2024-03-22 15:51:10.960064'); +INSERT INTO `sys_config` VALUES (7, 'inventory_number_prefix', '库存编号', 'S', '库存编号', '2024-03-25 15:54:08.836711', '2024-03-25 15:54:08.836711'); +INSERT INTO `sys_config` VALUES (8, 'app_version', 'app版本号', '1.0.1', 'app版本号', '2024-04-07 10:32:41.513615', '2024-04-07 10:32:41.513615'); +INSERT INTO `sys_config` VALUES (9, 'is_app_force_upgrade', 'app是否强制更新', '1', 'app是否强制更新。一般用于必须升级的更新需求。', '2024-04-07 10:33:07.732646', '2024-04-07 10:33:07.732646'); + +-- ---------------------------- +-- Table structure for sys_dept +-- ---------------------------- +DROP TABLE IF EXISTS `sys_dept`; +CREATE TABLE `sys_dept` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL, + `orderNo` int NULL DEFAULT 0, + `mpath` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT '', + `parentId` int NULL DEFAULT NULL, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`) USING BTREE, + INDEX `FK_c75280b01c49779f2323536db67`(`parentId`) USING BTREE, + CONSTRAINT `FK_c75280b01c49779f2323536db67` FOREIGN KEY (`parentId`) REFERENCES `sys_dept` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of sys_dept +-- ---------------------------- +INSERT INTO `sys_dept` VALUES (1, '山东矿机华能装备制造', 1, '1.', NULL, '2023-11-10 00:31:43.996025', '2024-04-02 14:57:50.000000'); +INSERT INTO `sys_dept` VALUES (2, '信息电控部', 1, '1.2.', 1, '2023-11-10 00:31:43.996025', '2024-04-02 14:58:04.000000'); +INSERT INTO `sys_dept` VALUES (10, '山东矿机华信智能科技', 1, '10.', NULL, '2024-04-07 10:33:31.759382', '2024-04-07 10:33:31.000000'); +INSERT INTO `sys_dept` VALUES (11, '计算机开发部', 0, '10.11.', 10, '2024-04-07 10:33:44.029857', '2024-04-07 10:33:44.000000'); + +-- ---------------------------- +-- Table structure for sys_dict +-- ---------------------------- +DROP TABLE IF EXISTS `sys_dict`; +CREATE TABLE `sys_dict` ( + `id` int NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `create_by` int NOT NULL COMMENT '创建者', + `update_by` int NOT NULL COMMENT '更新者', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `status` tinyint NOT NULL DEFAULT 1, + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `IDX_d112365748f740ee260b65ce91`(`name`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of sys_dict +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sys_dict_item +-- ---------------------------- +DROP TABLE IF EXISTS `sys_dict_item`; +CREATE TABLE `sys_dict_item` ( + `id` int NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `create_by` int NOT NULL COMMENT '创建者', + `update_by` int NOT NULL COMMENT '更新者', + `label` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `value` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `order` int NULL DEFAULT NULL COMMENT '字典项排序', + `status` tinyint NOT NULL DEFAULT 1, + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `type_id` int NULL DEFAULT NULL, + `orderNo` int NULL DEFAULT NULL COMMENT '字典项排序', + `deleted_at` datetime(6) NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`) USING BTREE, + INDEX `FK_d68ea74fcb041c8cfd1fd659844`(`type_id`) USING BTREE, + CONSTRAINT `FK_d68ea74fcb041c8cfd1fd659844` FOREIGN KEY (`type_id`) REFERENCES `sys_dict_type` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 49 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of sys_dict_item +-- ---------------------------- +INSERT INTO `sys_dict_item` VALUES (1, '2024-01-29 01:24:51.846135', '2024-01-29 02:23:19.000000', 1, 1, '男', '1', 0, 1, '性别男', 1, 3, '2024-03-01 15:28:21.702930'); +INSERT INTO `sys_dict_item` VALUES (2, '2024-01-29 01:32:58.458741', '2024-01-29 01:58:20.000000', 1, 1, '女', '0', 1, 1, '性别女', 1, 2, '2024-03-01 15:28:21.702930'); +INSERT INTO `sys_dict_item` VALUES (3, '2024-01-29 01:59:17.805394', '2024-01-29 14:37:18.000000', 1, 1, '人妖王', '3', NULL, 1, '安布里奥·伊万科夫', 1, 0, '2024-03-01 15:28:21.702930'); +INSERT INTO `sys_dict_item` VALUES (5, '2024-01-29 02:13:01.782466', '2024-01-29 02:13:01.782466', 1, 1, '显示', '1', NULL, 1, '显示菜单', 2, 0, '2024-03-01 15:28:21.702930'); +INSERT INTO `sys_dict_item` VALUES (6, '2024-01-29 02:13:31.134721', '2024-01-29 02:13:31.134721', 1, 1, '隐藏', '0', NULL, 1, '隐藏菜单', 2, 0, '2024-03-01 15:28:21.702930'); +INSERT INTO `sys_dict_item` VALUES (10, '2024-02-28 16:39:44.977246', '2024-02-29 15:56:02.670095', 1, 1, '商务合同', 'business', NULL, 1, '商务合同', 3, 0, '2024-03-01 15:28:21.702930'); +INSERT INTO `sys_dict_item` VALUES (11, '2024-02-28 16:42:43.539979', '2024-02-29 15:56:07.676659', 1, 1, '销售合同', 'sales', NULL, 1, '', 3, 1, '2024-03-01 15:28:21.702930'); +INSERT INTO `sys_dict_item` VALUES (12, '2024-02-28 16:42:58.224299', '2024-02-29 15:56:05.815675', 1, 1, '租赁合同', 'Lease', NULL, 1, NULL, 3, 2, '2024-03-01 15:28:21.702930'); +INSERT INTO `sys_dict_item` VALUES (13, '2024-02-28 16:43:26.311650', '2024-02-29 15:56:10.462447', 1, 1, '服务合同', 'service', NULL, 1, NULL, 3, 3, '2024-03-01 15:28:21.702930'); +INSERT INTO `sys_dict_item` VALUES (14, '2024-03-04 13:42:26.688441', '2024-03-04 13:42:26.688441', 1, 1, '件', 'unit_jian', NULL, 1, NULL, 5, 0, '2024-03-04 13:42:26.688441'); +INSERT INTO `sys_dict_item` VALUES (15, '2024-03-04 13:42:38.298733', '2024-03-04 13:42:38.298733', 1, 1, '个', 'unit_ge', NULL, 1, NULL, 5, 1, '2024-03-04 13:42:38.298733'); +INSERT INTO `sys_dict_item` VALUES (16, '2024-03-04 13:43:30.965353', '2024-03-04 13:43:30.965353', 1, 1, '千克', 'unit_qianke', NULL, 1, NULL, 5, 2, '2024-03-04 13:43:30.965353'); +INSERT INTO `sys_dict_item` VALUES (17, '2024-03-04 13:43:44.353125', '2024-03-04 13:43:44.353125', 1, 1, '克', 'unit_ke', NULL, 1, NULL, 5, 3, '2024-03-04 13:43:44.353125'); +INSERT INTO `sys_dict_item` VALUES (18, '2024-03-04 13:43:56.643339', '2024-03-04 13:43:56.643339', 1, 1, '升', 'unit_sheng', NULL, 1, NULL, 5, 4, '2024-03-04 13:43:56.643339'); +INSERT INTO `sys_dict_item` VALUES (19, '2024-03-04 13:44:09.242901', '2024-03-04 13:44:09.242901', 1, 1, '毫升', 'unit_haosheng', NULL, 1, NULL, 5, 5, '2024-03-04 13:44:09.242901'); +INSERT INTO `sys_dict_item` VALUES (20, '2024-03-04 13:44:26.620837', '2024-03-04 13:44:29.000000', 1, 1, '卷', 'unit_juan', NULL, 1, NULL, 5, 6, '2024-03-04 13:44:29.654314'); +INSERT INTO `sys_dict_item` VALUES (21, '2024-03-04 14:10:38.216659', '2024-03-04 14:10:54.000000', 1, 1, '批', 'unit_pi', NULL, 1, NULL, 5, 7, '2024-03-04 14:10:54.729114'); +INSERT INTO `sys_dict_item` VALUES (22, '2024-03-04 14:10:48.864655', '2024-03-04 14:10:48.864655', 1, 1, '片', 'unit_pian', NULL, 1, NULL, 5, 8, '2024-03-04 14:10:48.864655'); +INSERT INTO `sys_dict_item` VALUES (23, '2024-03-04 14:11:06.319281', '2024-03-04 14:11:06.319281', 1, 1, '套', 'unit_tao', NULL, 1, NULL, 5, 9, '2024-03-04 14:11:06.319281'); +INSERT INTO `sys_dict_item` VALUES (24, '2024-03-07 16:33:17.412474', '2024-03-07 16:33:17.412474', 1, 1, '奥迪A8(鲁UKS052)', 'car_1', NULL, 1, NULL, 6, 1, '2024-03-07 16:33:17.412474'); +INSERT INTO `sys_dict_item` VALUES (25, '2024-03-07 16:33:44.438153', '2024-03-07 16:33:44.438153', 1, 1, '阿尔法(鲁B33A52)', 'car_2', NULL, 1, NULL, 6, 1, '2024-03-07 16:33:44.438153'); +INSERT INTO `sys_dict_item` VALUES (26, '2024-03-07 16:34:08.872618', '2024-03-07 16:34:08.872618', 1, 1, '威尔法(鲁B33G21)', 'car_3', NULL, 1, NULL, 6, 2, '2024-03-07 16:34:08.872618'); +INSERT INTO `sys_dict_item` VALUES (27, '2024-03-25 08:28:13.363025', '2024-03-25 08:30:51.000000', 1, 1, '库房一', 'room_1', NULL, 1, NULL, 7, 0, '2024-03-25 08:30:51.792948'); +INSERT INTO `sys_dict_item` VALUES (28, '2024-03-25 08:28:23.806536', '2024-03-25 08:30:55.000000', 1, 1, '库房二', 'room_2', NULL, 1, NULL, 7, 1, '2024-03-25 08:30:55.408039'); +INSERT INTO `sys_dict_item` VALUES (29, '2024-03-25 08:28:31.643400', '2024-03-25 08:30:59.000000', 1, 1, '库房三', 'room_3', NULL, 1, NULL, 7, 2, '2024-03-25 08:30:59.195490'); +INSERT INTO `sys_dict_item` VALUES (30, '2024-03-25 08:29:49.485531', '2024-03-25 08:30:39.000000', 1, 1, '第一排', 'line_1', NULL, 1, NULL, 8, 0, '2024-03-25 08:30:39.156586'); +INSERT INTO `sys_dict_item` VALUES (31, '2024-03-25 08:29:58.991397', '2024-03-25 08:30:22.000000', 1, 1, '第二排', 'line_2', NULL, 1, NULL, 8, 1, '2024-03-25 08:30:22.398794'); +INSERT INTO `sys_dict_item` VALUES (32, '2024-03-25 08:30:09.155470', '2024-03-25 08:30:09.155470', 1, 1, '第三排', 'line_3', NULL, 1, NULL, 8, 2, '2024-03-25 08:30:09.155470'); +INSERT INTO `sys_dict_item` VALUES (33, '2024-03-25 08:30:18.716726', '2024-03-25 08:30:18.716726', 1, 1, '第四排', 'line_4', NULL, 1, NULL, 8, 3, '2024-03-25 08:30:18.716726'); +INSERT INTO `sys_dict_item` VALUES (34, '2024-03-25 08:30:33.674158', '2024-03-25 08:30:33.674158', 1, 1, '第五排', 'line_5', NULL, 1, NULL, 8, 4, '2024-03-25 08:30:33.674158'); +INSERT INTO `sys_dict_item` VALUES (35, '2024-03-25 08:32:06.027559', '2024-03-25 08:32:06.027559', 1, 1, '第一层', 'level_1', NULL, 1, NULL, 9, 0, '2024-03-25 08:32:06.027559'); +INSERT INTO `sys_dict_item` VALUES (36, '2024-03-25 08:32:14.302500', '2024-03-25 08:32:14.302500', 1, 1, '第二层', 'level_2', NULL, 1, NULL, 9, 1, '2024-03-25 08:32:14.302500'); +INSERT INTO `sys_dict_item` VALUES (37, '2024-03-25 08:32:54.412145', '2024-03-25 08:32:54.412145', 1, 1, '第三层', 'level_3', NULL, 1, NULL, 9, 2, '2024-03-25 08:32:54.412145'); +INSERT INTO `sys_dict_item` VALUES (38, '2024-03-25 08:33:02.567402', '2024-03-25 08:33:02.567402', 1, 1, '第四层', 'level_4', NULL, 1, NULL, 9, 3, '2024-03-25 08:33:02.567402'); +INSERT INTO `sys_dict_item` VALUES (39, '2024-03-25 08:33:12.209556', '2024-03-25 08:33:12.209556', 1, 1, '第五层', 'level_5', NULL, 1, NULL, 9, 4, '2024-03-25 08:33:12.209556'); +INSERT INTO `sys_dict_item` VALUES (40, '2024-04-02 12:26:18.720198', '2024-04-02 12:26:18.720198', 1, 1, '中间过渡架电控部分', 'sales_quotation_group_1', NULL, 1, NULL, 10, 0, '2024-04-02 12:26:18.720198'); +INSERT INTO `sys_dict_item` VALUES (41, '2024-04-02 12:26:26.985680', '2024-04-02 12:26:26.985680', 1, 1, '端头架电控部分', 'sales_quotation_group_2', NULL, 1, NULL, 10, 1, '2024-04-02 12:26:26.985680'); +INSERT INTO `sys_dict_item` VALUES (42, '2024-04-02 12:26:39.202760', '2024-04-02 12:26:39.202760', 1, 1, '主阀部分', 'sales_quotation_group_3', NULL, 1, NULL, 10, 2, '2024-04-02 12:26:39.202760'); +INSERT INTO `sys_dict_item` VALUES (43, '2024-04-02 12:26:45.611332', '2024-04-02 12:26:45.611332', 1, 1, '自动反冲洗过滤器部分', 'sales_quotation_group_4', NULL, 1, NULL, 10, 3, '2024-04-02 12:26:45.611332'); +INSERT INTO `sys_dict_item` VALUES (44, '2024-04-02 12:26:52.092188', '2024-04-02 12:26:52.092188', 1, 1, '位移测量部分', 'sales_quotation_group_5', NULL, 1, NULL, 10, 4, '2024-04-02 12:26:52.092188'); +INSERT INTO `sys_dict_item` VALUES (45, '2024-04-02 12:26:59.178581', '2024-04-02 12:26:59.178581', 1, 1, '压力检测部分', 'sales_quotation_group_6', NULL, 1, NULL, 10, 5, '2024-04-02 12:26:59.178581'); +INSERT INTO `sys_dict_item` VALUES (46, '2024-04-02 12:27:05.494866', '2024-04-02 12:27:05.494866', 1, 1, '煤机定位部分', 'sales_quotation_group_7', NULL, 1, NULL, 10, 6, '2024-04-02 12:27:05.494866'); +INSERT INTO `sys_dict_item` VALUES (47, '2024-04-02 12:27:12.313251', '2024-04-02 12:27:12.313251', 1, 1, '姿态检测部分', 'sales_quotation_group_8', NULL, 1, NULL, 10, 7, '2024-04-02 12:27:12.313251'); +INSERT INTO `sys_dict_item` VALUES (48, '2024-04-02 15:01:53.004626', '2024-04-02 15:01:53.004626', 1, 1, 'A区', 'areaA', NULL, 1, NULL, 8, 1, '2024-04-02 15:01:53.004626'); + +-- ---------------------------- +-- Table structure for sys_dict_type +-- ---------------------------- +DROP TABLE IF EXISTS `sys_dict_type`; +CREATE TABLE `sys_dict_type` ( + `id` int NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `create_by` int NOT NULL COMMENT '创建者', + `update_by` int NOT NULL COMMENT '更新者', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `status` tinyint NOT NULL DEFAULT 1, + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `deleted_at` datetime(6) NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `IDX_74d0045ff7fab9f67adc0b1bda`(`code`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of sys_dict_type +-- ---------------------------- +INSERT INTO `sys_dict_type` VALUES (1, '2024-01-28 08:19:12.777447', '2024-02-08 13:05:10.000000', 1, 1, '性别', 1, '性别单选', 'sys_user_gender', '2024-03-01 15:28:21.689753'); +INSERT INTO `sys_dict_type` VALUES (2, '2024-01-28 08:38:41.235185', '2024-01-29 02:11:33.000000', 1, 1, '菜单显示状态', 1, '菜单显示状态', 'sys_show_hide', '2024-03-01 15:28:21.689753'); +INSERT INTO `sys_dict_type` VALUES (3, '2024-02-28 16:38:27.311577', '2024-03-04 13:26:29.000000', 1, 1, '合同类型', 1, '合同类型', 'contract_type', '2024-03-04 13:26:29.911469'); +INSERT INTO `sys_dict_type` VALUES (5, '2024-03-04 13:41:05.156027', '2024-04-05 10:12:14.000000', 1, 9, '单位', 1, '材料盘点表等单位。件。个。台', 'unit', '2024-04-05 10:12:14.058367'); +INSERT INTO `sys_dict_type` VALUES (6, '2024-03-07 16:32:26.985730', '2024-03-07 16:32:26.985730', 1, 1, '公司车辆', 1, '公司的公车', 'vehicle', '2024-03-07 16:32:26.985730'); +INSERT INTO `sys_dict_type` VALUES (7, '2024-03-25 08:27:37.461575', '2024-03-25 08:27:37.461575', 1, 1, '库存位置-房间', 1, '库存存放的房间', 'inventory_room', '2024-03-25 08:27:37.461575'); +INSERT INTO `sys_dict_type` VALUES (8, '2024-03-25 08:29:29.110447', '2024-03-25 08:29:29.110447', 1, 1, '库存位置-排架号', 1, '库存位置-排架号', 'inventory_line', '2024-03-25 08:29:29.110447'); +INSERT INTO `sys_dict_type` VALUES (9, '2024-03-25 08:31:52.669289', '2024-03-25 08:31:52.669289', 1, 1, '库存位置-层数', 1, NULL, 'inventory_line_level', '2024-03-25 08:31:52.669289'); +INSERT INTO `sys_dict_type` VALUES (10, '2024-04-02 12:25:40.233758', '2024-04-02 12:25:40.233758', 1, 1, '电解控报价计算-分组', 1, NULL, 'sale_quotation_group', '2024-04-02 12:25:40.233758'); + +-- ---------------------------- +-- Table structure for sys_login_log +-- ---------------------------- +DROP TABLE IF EXISTS `sys_login_log`; +CREATE TABLE `sys_login_log` ( + `id` int NOT NULL AUTO_INCREMENT, + `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `ua` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `provider` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `user_id` int NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `FK_3029712e0df6a28edaee46fd470`(`user_id`) USING BTREE, + CONSTRAINT `FK_3029712e0df6a28edaee46fd470` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 72 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of sys_login_log +-- ---------------------------- +INSERT INTO `sys_login_log` VALUES (1, '144.0.23.133', 'Dart/3.2 (dart:io)', '山东省青岛市', NULL, '2024-04-01 08:50:32.291716', '2024-04-01 08:50:32.291716', 1); +INSERT INTO `sys_login_log` VALUES (2, '144.0.23.133', 'Dart/3.3 (dart:io)', '山东省青岛市', NULL, '2024-04-01 14:37:00.079842', '2024-04-01 14:37:00.079842', 1); +INSERT INTO `sys_login_log` VALUES (3, '144.0.23.133', 'Dart/3.3 (dart:io)', '山东省青岛市', NULL, '2024-04-01 15:11:30.159405', '2024-04-01 15:11:30.159405', 1); +INSERT INTO `sys_login_log` VALUES (4, '144.0.23.133', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0', '山东省青岛市', NULL, '2024-04-01 15:15:45.157349', '2024-04-01 15:15:45.157349', 1); +INSERT INTO `sys_login_log` VALUES (5, '144.0.23.133', 'Dart/3.3 (dart:io)', '山东省青岛市', NULL, '2024-04-01 15:34:38.228762', '2024-04-01 15:34:38.228762', 1); +INSERT INTO `sys_login_log` VALUES (6, '144.0.23.133', 'Dart/3.3 (dart:io)', '山东省青岛市', NULL, '2024-04-01 15:41:22.061919', '2024-04-01 15:41:22.061919', 1); +INSERT INTO `sys_login_log` VALUES (7, '144.0.23.133', 'Dart/3.3 (dart:io)', '山东省青岛市', NULL, '2024-04-01 15:44:13.373410', '2024-04-01 15:44:13.373410', 1); +INSERT INTO `sys_login_log` VALUES (8, '144.0.23.133', 'Dart/3.3 (dart:io)', '山东省青岛市', NULL, '2024-04-01 15:47:23.396192', '2024-04-01 15:47:23.396192', 1); +INSERT INTO `sys_login_log` VALUES (9, '144.0.23.133', 'Dart/3.3 (dart:io)', '山东省青岛市', NULL, '2024-04-01 16:16:31.291096', '2024-04-01 16:16:31.291096', 1); +INSERT INTO `sys_login_log` VALUES (10, '144.0.23.133', 'Dart/3.3 (dart:io)', '山东省青岛市', NULL, '2024-04-01 16:21:30.780126', '2024-04-01 16:21:30.780126', 1); +INSERT INTO `sys_login_log` VALUES (11, '144.0.23.133', 'Dart/3.2 (dart:io)', '山东省青岛市', NULL, '2024-04-01 16:49:57.352530', '2024-04-01 16:49:57.352530', 1); +INSERT INTO `sys_login_log` VALUES (12, '144.0.23.133', 'Dart/3.2 (dart:io)', '山东省青岛市', NULL, '2024-04-01 16:50:13.073327', '2024-04-01 16:50:13.073327', 1); +INSERT INTO `sys_login_log` VALUES (13, '223.104.195.74', 'Dart/3.2 (dart:io)', '山东省潍坊市', NULL, '2024-04-01 17:16:04.439246', '2024-04-01 17:16:04.439246', 1); +INSERT INTO `sys_login_log` VALUES (14, '144.0.23.133', 'Dart/3.2 (dart:io)', '山东省青岛市', NULL, '2024-04-01 17:30:31.958849', '2024-04-01 17:30:31.958849', 1); +INSERT INTO `sys_login_log` VALUES (15, '221.1.97.166', 'Dart/3.2 (dart:io)', '山东省潍坊市昌乐县', NULL, '2024-04-02 10:09:56.131711', '2024-04-02 10:09:56.131711', 1); +INSERT INTO `sys_login_log` VALUES (16, '221.1.97.166', 'Dart/3.2 (dart:io)', '山东省潍坊市昌乐县', NULL, '2024-04-02 10:19:42.684419', '2024-04-02 10:19:42.684419', 1); +INSERT INTO `sys_login_log` VALUES (17, '112.224.65.182', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '山东省济南市', NULL, '2024-04-02 14:56:57.968284', '2024-04-02 14:56:57.968284', 1); +INSERT INTO `sys_login_log` VALUES (18, '112.224.65.182', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '山东省济南市', NULL, '2024-04-02 14:59:28.837362', '2024-04-02 14:59:28.837362', 9); +INSERT INTO `sys_login_log` VALUES (19, '112.224.65.182', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '山东省济南市', NULL, '2024-04-02 14:59:48.312224', '2024-04-02 14:59:48.312224', 1); +INSERT INTO `sys_login_log` VALUES (20, '221.1.97.166', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '山东省潍坊市昌乐县', NULL, '2024-04-02 15:38:45.330867', '2024-04-02 15:38:45.330867', 1); +INSERT INTO `sys_login_log` VALUES (21, '221.1.97.166', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '山东省潍坊市昌乐县', NULL, '2024-04-02 15:39:52.649278', '2024-04-02 15:39:52.649278', 10); +INSERT INTO `sys_login_log` VALUES (22, '221.1.97.166', 'Dart/3.2 (dart:io)', '山东省潍坊市昌乐县', NULL, '2024-04-02 15:40:20.394755', '2024-04-02 15:40:20.394755', 10); +INSERT INTO `sys_login_log` VALUES (23, '221.1.97.166', 'Dart/3.2 (dart:io)', '山东省潍坊市昌乐县', NULL, '2024-04-02 16:04:31.143028', '2024-04-02 16:04:31.143028', 1); +INSERT INTO `sys_login_log` VALUES (24, '112.224.65.182', 'Dart/3.3 (dart:io)', '山东省济南市', NULL, '2024-04-02 16:18:52.597148', '2024-04-02 16:18:52.597148', 1); +INSERT INTO `sys_login_log` VALUES (25, '112.224.65.182', 'Dart/3.3 (dart:io)', '山东省济南市', NULL, '2024-04-02 16:21:03.861179', '2024-04-02 16:21:03.861179', 1); +INSERT INTO `sys_login_log` VALUES (26, '221.1.97.166', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '山东省潍坊市昌乐县', NULL, '2024-04-02 16:22:06.193594', '2024-04-02 16:22:06.193594', 1); +INSERT INTO `sys_login_log` VALUES (27, '221.1.97.166', 'Dart/3.3 (dart:io)', '山东省潍坊市昌乐县', NULL, '2024-04-02 16:27:03.753690', '2024-04-02 16:27:03.753690', 1); +INSERT INTO `sys_login_log` VALUES (28, '221.1.97.166', 'Dart/3.3 (dart:io)', '山东省潍坊市昌乐县', NULL, '2024-04-02 16:34:35.796027', '2024-04-02 16:34:35.796027', 1); +INSERT INTO `sys_login_log` VALUES (29, '221.1.97.166', 'Dart/3.3 (dart:io)', '山东省潍坊市昌乐县', NULL, '2024-04-02 16:35:12.372648', '2024-04-02 16:35:12.372648', 1); +INSERT INTO `sys_login_log` VALUES (30, '221.1.97.166', 'Dart/3.3 (dart:io)', '山东省潍坊市昌乐县', NULL, '2024-04-02 16:51:18.226019', '2024-04-02 16:51:18.226019', 1); +INSERT INTO `sys_login_log` VALUES (31, '221.1.97.166', 'Dart/3.2 (dart:io)', '山东省潍坊市昌乐县', NULL, '2024-04-02 16:52:11.095661', '2024-04-02 16:52:11.095661', 1); +INSERT INTO `sys_login_log` VALUES (32, '221.1.97.166', 'Dart/3.2 (dart:io)', '山东省潍坊市昌乐县', NULL, '2024-04-02 16:52:46.246857', '2024-04-02 16:52:46.246857', 1); +INSERT INTO `sys_login_log` VALUES (33, '223.104.195.90', 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.48(0x1800302c) NetType/4G Language/zh_CN', '山东省潍坊市', NULL, '2024-04-02 18:06:51.031947', '2024-04-02 18:06:51.031947', 10); +INSERT INTO `sys_login_log` VALUES (34, '17.232.78.156', 'Dart/3.3 (dart:io)', '美国Apple', NULL, '2024-04-02 20:38:53.231472', '2024-04-02 20:38:53.231472', 1); +INSERT INTO `sys_login_log` VALUES (35, '221.1.97.166', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 NetType/WIFI MicroMessenger/7.0.20.1781(0x6700143B) WindowsWechat(0x63090a13) XWEB/9079 Flue', '山东省潍坊市昌乐县', NULL, '2024-04-03 08:08:44.705984', '2024-04-03 08:08:44.705984', 1); +INSERT INTO `sys_login_log` VALUES (36, '221.1.97.166', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 NetType/WIFI MicroMessenger/7.0.20.1781(0x6700143B) WindowsWechat(0x6309092b) XWEB/9079 Flue', '山东省潍坊市昌乐县', NULL, '2024-04-03 08:09:10.249023', '2024-04-03 08:09:10.249023', 9); +INSERT INTO `sys_login_log` VALUES (37, '144.0.23.133', 'Dart/3.2 (dart:io)', '山东省青岛市', NULL, '2024-04-03 15:15:03.718007', '2024-04-03 15:15:03.718007', 1); +INSERT INTO `sys_login_log` VALUES (38, '144.0.23.133', 'Dart/3.2 (dart:io)', '山东省青岛市', NULL, '2024-04-03 15:38:42.155118', '2024-04-03 15:38:42.155118', 1); +INSERT INTO `sys_login_log` VALUES (39, '221.1.97.166', 'Dart/3.2 (dart:io)', '山东省潍坊市昌乐县', NULL, '2024-04-03 18:25:46.887892', '2024-04-03 18:25:46.887892', 1); +INSERT INTO `sys_login_log` VALUES (40, '221.1.97.166', 'Dart/3.2 (dart:io)', '山东省潍坊市昌乐县', NULL, '2024-04-04 13:54:25.249419', '2024-04-04 13:54:25.249419', 1); +INSERT INTO `sys_login_log` VALUES (41, '112.224.195.64', 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.48(0x1800302c) NetType/4G Language/zh_CN', '山东省济南市', NULL, '2024-04-04 13:55:42.245768', '2024-04-04 13:55:42.245768', 1); +INSERT INTO `sys_login_log` VALUES (42, '221.1.97.166', 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.47(0x18002f2c) NetType/4G Language/zh_CN', '山东省潍坊市昌乐县', NULL, '2024-04-04 13:58:47.699965', '2024-04-04 13:58:47.699965', 1); +INSERT INTO `sys_login_log` VALUES (43, '221.1.97.166', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/89.0.4389.116 Safari/534.24 Device/pipa Model/23043RP34C XiaoMi/MiuiBrowser/14.7.76', '山东省潍坊市昌乐县', NULL, '2024-04-04 14:00:07.465436', '2024-04-04 14:00:07.465436', 1); +INSERT INTO `sys_login_log` VALUES (44, '221.1.97.166', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/89.0.4389.116 Safari/534.24 Device/pipa Model/23043RP34C XiaoMi/MiuiBrowser/14.7.76', '山东省潍坊市昌乐县', NULL, '2024-04-04 14:00:17.602620', '2024-04-04 14:00:17.602620', 1); +INSERT INTO `sys_login_log` VALUES (45, '221.1.97.166', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/89.0.4389.116 Safari/534.24 Device/pipa Model/23043RP34C XiaoMi/MiuiBrowser/14.7.76', '山东省潍坊市昌乐县', NULL, '2024-04-04 14:00:42.762900', '2024-04-04 14:00:42.762900', 1); +INSERT INTO `sys_login_log` VALUES (46, '221.1.97.166', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/89.0.4389.116 Safari/534.24 Device/pipa Model/23043RP34C XiaoMi/MiuiBrowser/14.7.76', '山东省潍坊市昌乐县', NULL, '2024-04-04 14:01:48.207951', '2024-04-04 14:01:48.207951', 1); +INSERT INTO `sys_login_log` VALUES (47, '221.1.97.166', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/89.0.4389.116 Safari/534.24 Device/pipa Model/23043RP34C XiaoMi/MiuiBrowser/14.7.76', '山东省潍坊市昌乐县', NULL, '2024-04-04 14:02:33.248728', '2024-04-04 14:02:33.248728', 1); +INSERT INTO `sys_login_log` VALUES (48, '221.1.97.166', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/89.0.4389.116 Safari/534.24 Device/pipa Model/23043RP34C XiaoMi/MiuiBrowser/14.7.76', '山东省潍坊市昌乐县', NULL, '2024-04-04 14:02:49.821556', '2024-04-04 14:02:49.821556', 1); +INSERT INTO `sys_login_log` VALUES (49, '221.1.97.166', 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Mobile/15E148 Safari/604.1', '山东省潍坊市昌乐县', NULL, '2024-04-04 14:03:01.518252', '2024-04-04 14:03:01.518252', 1); +INSERT INTO `sys_login_log` VALUES (50, '221.1.97.166', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/89.0.4389.116 Safari/534.24 Device/pipa Model/23043RP34C XiaoMi/MiuiBrowser/14.7.76', '山东省潍坊市昌乐县', NULL, '2024-04-04 14:04:11.038817', '2024-04-04 14:04:11.038817', 1); +INSERT INTO `sys_login_log` VALUES (51, '221.1.97.166', 'Mozilla/5.0 (Linux; U; Android 13; zh-CN; 23043RP34C Build/TKQ1.221114.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.58 Quark/6.11.0.530 Mobile Safari/537.36', '山东省潍坊市昌乐县', NULL, '2024-04-04 14:06:02.846849', '2024-04-04 14:06:02.846849', 1); +INSERT INTO `sys_login_log` VALUES (52, '221.1.97.166', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '山东省潍坊市昌乐县', NULL, '2024-04-04 14:06:05.939892', '2024-04-04 14:06:05.939892', 1); +INSERT INTO `sys_login_log` VALUES (53, '223.104.195.112', 'Dart/3.2 (dart:io)', '山东省潍坊市', NULL, '2024-04-05 08:58:35.158404', '2024-04-05 08:58:35.158404', 1); +INSERT INTO `sys_login_log` VALUES (54, '221.1.97.166', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '山东省潍坊市昌乐县', NULL, '2024-04-05 14:54:34.211950', '2024-04-05 14:54:34.211950', 1); +INSERT INTO `sys_login_log` VALUES (55, '112.226.20.42', 'Dart/3.3 (dart:io)', '山东省青岛市', NULL, '2024-04-06 23:33:40.648379', '2024-04-06 23:33:40.648379', 1); +INSERT INTO `sys_login_log` VALUES (56, '144.0.23.133', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '山东省青岛市', NULL, '2024-04-07 10:25:18.662022', '2024-04-07 10:25:18.662022', 1); +INSERT INTO `sys_login_log` VALUES (57, '144.0.23.133', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '山东省青岛市', NULL, '2024-04-07 10:43:27.092753', '2024-04-07 10:43:27.092753', 1); +INSERT INTO `sys_login_log` VALUES (58, '127.0.0.1', 'Dart/3.2 (dart:io)', '内网IP', NULL, '2024-04-07 10:48:59.426068', '2024-04-07 10:48:59.426068', 9); +INSERT INTO `sys_login_log` VALUES (59, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '内网IP', NULL, '2024-04-07 10:49:55.017103', '2024-04-07 10:49:55.017103', 9); +INSERT INTO `sys_login_log` VALUES (60, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '内网IP', NULL, '2024-04-07 10:50:14.432721', '2024-04-07 10:50:14.432721', 1); +INSERT INTO `sys_login_log` VALUES (61, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '内网IP', NULL, '2024-04-07 10:51:14.143775', '2024-04-07 10:51:14.143775', 9); +INSERT INTO `sys_login_log` VALUES (62, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '内网IP', NULL, '2024-04-07 10:52:21.472549', '2024-04-07 10:52:21.472549', 1); +INSERT INTO `sys_login_log` VALUES (63, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '内网IP', NULL, '2024-04-07 10:53:25.410445', '2024-04-07 10:53:25.410445', 9); +INSERT INTO `sys_login_log` VALUES (64, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '内网IP', NULL, '2024-04-07 10:54:11.422209', '2024-04-07 10:54:11.422209', 1); +INSERT INTO `sys_login_log` VALUES (65, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '内网IP', NULL, '2024-04-07 10:58:09.630300', '2024-04-07 10:58:09.630300', 1); +INSERT INTO `sys_login_log` VALUES (66, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '内网IP', NULL, '2024-04-07 10:58:48.213228', '2024-04-07 10:58:48.213228', 9); +INSERT INTO `sys_login_log` VALUES (67, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '内网IP', NULL, '2024-04-07 10:59:16.336015', '2024-04-07 10:59:16.336015', 9); +INSERT INTO `sys_login_log` VALUES (68, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '内网IP', NULL, '2024-04-07 10:59:52.597361', '2024-04-07 10:59:52.597361', 9); +INSERT INTO `sys_login_log` VALUES (69, '127.0.0.1', 'Dart/3.2 (dart:io)', '内网IP', NULL, '2024-04-07 11:00:39.570222', '2024-04-07 11:00:39.570222', 9); +INSERT INTO `sys_login_log` VALUES (70, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '内网IP', NULL, '2024-04-07 11:01:59.666647', '2024-04-07 11:01:59.666647', 1); +INSERT INTO `sys_login_log` VALUES (71, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', '内网IP', NULL, '2024-04-07 11:07:30.060388', '2024-04-07 11:07:30.060388', 1); + +-- ---------------------------- +-- Table structure for sys_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sys_menu`; +CREATE TABLE `sys_menu` ( + `id` int NOT NULL AUTO_INCREMENT, + `parent_id` int NULL DEFAULT NULL, + `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `permission` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `type` tinyint NOT NULL DEFAULT 0, + `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '', + `order_no` int NULL DEFAULT 0, + `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `keep_alive` tinyint NOT NULL DEFAULT 1, + `show` tinyint NOT NULL DEFAULT 1, + `status` tinyint NOT NULL DEFAULT 1, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `is_ext` tinyint NOT NULL DEFAULT 0, + `ext_open_mode` tinyint NOT NULL DEFAULT 1, + `active_menu` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 167 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of sys_menu +-- ---------------------------- +INSERT INTO `sys_menu` VALUES (1, NULL, '/system', '系统管理', '', 0, 'ant-design:setting-outlined', 254, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-02-29 10:41:29.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (2, 1, '/system/user', '用户管理', 'system:user:list', 1, 'ant-design:user-outlined', 0, 'system/user/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-17 03:10:30.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (3, 1, '/system/role', '角色管理', 'system:role:list', 1, 'ep:user', 1, 'system/role/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-17 03:11:02.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (4, 1, '/system/menu', '菜单管理', 'system:menu:list', 1, 'ep:menu', 2, 'system/menu/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-17 03:11:18.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (5, 1, '/system/monitor', '系统监控', '', 0, 'ep:monitor', 5, '', 1, 0, 1, '2023-11-10 00:31:44.023393', '2024-04-02 15:00:20.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (6, 5, '/system/monitor/online', '在线用户', 'system:online:list', 1, '', 0, 'system/monitor/online/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-15 22:13:59.519267', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (7, 5, '/sys/monitor/login-log', '登录日志', 'system:log:login:list', 1, '', 0, 'system/monitor/log/login/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-15 22:14:02.610719', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (8, 5, '/system/monitor/serve', '服务监控', 'system:serve:stat', 1, '', 4, 'system/monitor/serve/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-15 22:14:05.606355', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (9, 1, '/system/schedule', '任务调度', '', 0, 'ant-design:schedule-filled', 6, '', 1, 0, 1, '2023-11-10 00:31:44.023393', '2024-04-02 15:00:23.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (10, 9, '/system/task', '任务管理', '', 1, '', 0, 'system/schedule/task/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-17 03:14:39.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (11, 9, '/system/task/log', '任务日志', 'system:task:list', 1, '', 0, 'system/schedule/log/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-17 03:15:01.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (12, NULL, '/document', '文档', '', 0, 'ion:tv-outline', 2, '', 1, 0, 1, '2023-11-10 00:31:44.023393', '2024-02-28 11:51:51.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (14, 12, 'https://www.typeorm.org/', 'Typeorm中文文档(外链)', NULL, 1, '', 3, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-30 18:39:53.000000', 1, 1, NULL); +INSERT INTO `sys_menu` VALUES (15, 12, 'https://docs.nestjs.cn/', 'Nest.js中文文档(内嵌)', '', 1, '', 4, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-30 18:40:43.000000', 1, 2, NULL); +INSERT INTO `sys_menu` VALUES (20, 2, NULL, '新增', 'system:user:create', 2, '', 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (21, 2, '', '删除', 'system:user:delete', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (22, 2, '', '更新', 'system:user:update', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (23, 2, '', '查询', 'system:user:read', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (24, 3, '', '新增', 'system:role:create', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (25, 3, '', '删除', 'system:role:delete', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (26, 3, '', '修改', 'system:role:update', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (27, 3, '', '查询', 'system:role:read', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (28, 4, NULL, '新增', 'system:menu:create', 2, NULL, 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (29, 4, NULL, '删除', 'system:menu:delete', 2, NULL, 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (30, 4, '', '修改', 'system:menu:update', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (31, 4, NULL, '查询', 'system:menu:read', 2, NULL, 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (32, 6, '', '下线', 'system:online:kick', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (34, 10, '', '新增', 'system:task:create', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (35, 10, '', '删除', 'system:task:delete', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (36, 10, '', '执行一次', 'system:task:once', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (37, 10, '', '查询', 'system:task:read', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (38, 10, '', '运行', 'system:task:start', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (39, 10, '', '暂停', 'system:task:stop', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (40, 10, '', '更新', 'system:task:update', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (41, 7, '', '查询登录日志', 'system:log:login:list', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (42, 7, '', '查询任务日志', 'system:log:task:list', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (43, NULL, '/about', '关于', '', 1, 'ant-design:info-circle-outlined', 260, 'account/about', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-02-10 09:35:41.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (48, NULL, '/tool', '系统工具', NULL, 0, 'ant-design:tool-outlined', 255, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-02-29 10:41:25.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (49, 48, '/tool/email', '邮件工具', 'system:tools:email', 1, 'ant-design:send-outlined', 1, 'tool/email/index', 1, 0, 1, '2023-11-10 00:31:44.023393', '2024-02-28 11:51:38.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (50, 49, NULL, '发送邮件', 'tools:email:send', 2, '', 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (51, 48, '/tool/storage', '本地存储管理', 'tool:storage:list', 1, 'ant-design:appstore-outlined', 2, 'tool/storage/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-03-08 15:13:32.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (52, 51, NULL, '文件上传', 'upload:upload', 2, '', 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-25 01:04:08.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (53, 51, NULL, '文件删除', 'tool:storage:delete', 2, '', 2, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-25 00:56:01.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (54, 2, NULL, '修改密码', 'system:user:password', 2, '', 5, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (56, 1, '/system/dict-type', '字典管理', 'system:dict-type:list', 1, 'ant-design:book-outlined', 4, 'system/dict-type/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-28 09:07:12.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (57, 56, NULL, '新增', 'system:dict-type:create', 2, '', 1, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-28 09:07:20.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (58, 56, NULL, '更新', 'system:dict-type:update', 2, '', 2, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-28 09:07:26.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (59, 56, NULL, '删除', 'system:dict-type:delete', 2, '', 3, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-28 09:07:42.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (60, 56, NULL, '查询', 'system:dict-type:info', 2, '', 4, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-28 09:07:36.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (61, 1, '/system/dept', '部门管理', 'system:dept:list', 1, 'ant-design:deployment-unit-outlined', 3, 'system/dept/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-17 03:11:55.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (62, 61, NULL, '新增', 'system:dept:create', 2, '', 1, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (63, 61, NULL, '更新', 'system:dept:update', 2, '', 2, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (64, 61, NULL, '删除', 'system:dept:delete', 2, '', 3, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (65, 61, NULL, '查询', 'system:dept:read', 2, '', 4, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (68, 5, '/health', '健康检查', '', 1, '', 4, '', 1, 0, 1, '2023-11-10 00:31:44.023393', '2024-01-27 18:53:33.352155', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (69, 68, NULL, '网络', 'app:health:network', 2, '', 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (70, 68, NULL, '数据库', 'app:health: database', 2, '', 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (86, 1, '/param-config', '参数配置', 'system:param-config:list', 1, 'ep:edit', 255, 'system/param-config/index', 0, 1, 1, '2024-01-10 17:34:52.569663', '2024-01-19 02:11:27.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (87, 86, NULL, '查询', 'system:param-config:read', 2, '', 255, NULL, 1, 1, 1, '2024-01-10 17:39:20.983241', '2024-01-10 17:39:20.983241', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (88, 86, NULL, '新增', 'system:param-config:create', 2, '', 255, NULL, 1, 1, 1, '2024-01-10 17:39:57.543510', '2024-01-10 17:39:57.543510', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (89, 86, NULL, '更新', 'system:param-config:update', 2, '', 255, NULL, 1, 1, 1, '2024-01-10 17:40:27.355944', '2024-01-10 17:40:27.355944', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (92, 86, NULL, '删除', 'system:param-config:delete', 2, '', 255, NULL, 1, 1, 1, '2024-01-10 17:57:32.059887', '2024-01-10 17:57:32.059887', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (107, 1, 'system/dict-item/:id', '字典项管理', 'system:dict-item:list', 1, 'ant-design:facebook-outlined', 255, 'system/dict-item/index', 0, 0, 1, '2024-01-28 09:21:17.409532', '2024-01-30 13:09:47.000000', 0, 1, '字典管理'); +INSERT INTO `sys_menu` VALUES (108, 107, NULL, '新增', 'system:dict-item:create', 2, '', 255, NULL, 1, 1, 1, '2024-01-28 09:22:39.401758', '2024-01-28 22:38:36.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (109, 107, NULL, '更新', 'system:dict-item:update', 2, '', 255, NULL, 1, 1, 1, '2024-01-28 09:26:43.911886', '2024-01-28 09:26:43.911886', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (110, 107, NULL, '删除', 'system:dict-item:delete', 2, '', 255, NULL, 1, 1, 1, '2024-01-28 09:27:28.535225', '2024-01-28 09:27:28.535225', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (111, 107, NULL, '查询', 'system:dict-item:info', 2, '', 255, NULL, 1, 1, 1, '2024-01-28 09:27:43.894820', '2024-01-28 09:27:43.894820', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (112, 12, 'https://antdv.com/components/overview-cn', 'antdv文档(内嵌)', NULL, 1, '', 255, NULL, 1, 1, 1, '2024-01-29 09:23:08.407723', '2024-01-30 18:41:19.000000', 1, 2, NULL); +INSERT INTO `sys_menu` VALUES (115, NULL, 'netdisk', '网盘管理', NULL, 0, 'ant-design:cloud-server-outlined', 255, NULL, 1, 0, 1, '2024-02-10 08:00:02.394616', '2024-03-08 15:14:06.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (116, 48, 'manage', '云盘管理', 'netdisk:manage:list', 1, 'ant-design:cloud-server-outlined', 252, 'netdisk/manage', 0, 0, 1, '2024-02-10 08:03:49.837348', '2024-04-07 10:29:44.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (117, 116, NULL, '创建文件或文件夹', 'netdisk:manage:create', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:40:22.317257', '2024-02-10 08:40:22.317257', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (118, 116, NULL, '查看文件', 'netdisk:manage:read', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:41:22.008015', '2024-02-10 08:41:22.008015', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (119, 116, NULL, '更新', 'netdisk:manage:update', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:41:50.691643', '2024-02-10 08:41:50.691643', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (120, 116, NULL, '删除', 'netdisk:manage:delete', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:42:09.480601', '2024-02-10 08:42:09.480601', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (121, 116, NULL, '获取文件上传token', 'netdisk:manage:token', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:42:57.688104', '2024-02-10 08:42:57.688104', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (122, 116, NULL, '添加文件备注', 'netdisk:manage:mark', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:43:40.117321', '2024-02-10 08:43:40.117321', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (123, 116, NULL, '下载文件', 'netdisk:manage:download', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:44:01.338984', '2024-02-10 08:44:01.338984', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (124, 116, NULL, '重命名文件或文件夹', 'netdisk:manage:rename', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:44:27.233379', '2024-02-10 08:45:36.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (125, 116, NULL, '复制文件或文件夹', 'netdisk:manage:copy', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:44:44.725391', '2024-02-10 08:45:48.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (126, 116, NULL, '剪切文件或文件夹', 'netdisk:manage:cut', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:45:21.660511', '2024-02-10 08:45:21.660511', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (127, 115, 'overview', '网盘概览', 'netdisk:overview:desc', 1, '', 254, 'netdisk/overview', 0, 1, 1, '2024-02-10 09:32:56.981190', '2024-02-10 09:34:18.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (128, NULL, '/contract', '合同管理', NULL, 0, 'ep:document', 1, NULL, 1, 0, 1, '2024-02-29 10:40:39.080419', '2024-04-02 14:52:28.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (129, 128, '/contract/index', '合同审核', 'app:contract:list', 1, 'ep:document', 1, 'contract/index', 0, 1, 1, '2024-02-29 10:46:09.245521', '2024-02-29 14:59:56.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (130, NULL, '/vehicle-usage/index', '车辆使用', NULL, 1, 'ant-design:car-outlined', 4, 'vehicle-usage/index', 0, 0, 1, '2024-02-29 10:48:35.035363', '2024-04-02 14:53:14.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (131, NULL, '/materials-inventory/record-in-out', '出入库记录', 'materials_inventory:history_in_out:list', 1, 'ep:coin', 3, 'materials-inventory/in-out/index', 0, 1, 1, '2024-02-29 11:03:49.710130', '2024-04-02 14:52:54.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (132, 129, NULL, '更新', 'app:contract:update', 2, '', 255, NULL, 1, 1, 1, '2024-02-29 15:00:39.641043', '2024-02-29 15:00:39.641043', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (133, 129, NULL, '删除', 'app:contract:delete', 2, '', 255, NULL, 1, 1, 1, '2024-02-29 15:00:59.376071', '2024-02-29 15:00:59.376071', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (134, 129, NULL, '查询', 'app:contract:read', 2, '', 255, NULL, 1, 1, 1, '2024-02-29 15:01:14.209396', '2024-02-29 15:45:29.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (135, 129, NULL, '新增', 'app:contract:create', 2, '', 255, NULL, 1, 1, 1, '2024-02-29 15:44:46.950582', '2024-02-29 15:44:46.950582', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (136, 131, NULL, '新增', 'materials_inventory:history_in_out:create', 2, '', 255, NULL, 1, 1, 1, '2024-03-01 17:17:02.597782', '2024-03-06 10:54:28.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (137, 131, NULL, '更新', 'materials_inventory:history_in_out:update', 2, '', 255, NULL, 1, 1, 1, '2024-03-01 17:17:15.192910', '2024-03-06 10:54:57.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (138, 131, NULL, '查询单个', 'app:contract:read', 2, '', 255, NULL, 1, 1, 1, '2024-03-01 17:17:32.488892', '2024-03-01 17:17:32.488892', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (139, 131, NULL, '删除', 'materials_inventory:history_in_out:delete', 2, '', 255, NULL, 1, 1, 1, '2024-03-01 17:17:43.455773', '2024-03-06 10:55:06.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (140, NULL, '/materials-inventory/company', '乙方公司管理', 'app:company:list', 1, 'ep:office-building', 6, 'materials-inventory/company/index', 0, 1, 1, '2024-03-04 15:44:30.769048', '2024-03-27 12:57:31.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (141, 140, NULL, '单个查询', 'app:company:read', 2, '', 1, NULL, 1, 1, 1, '2024-03-04 15:45:55.979802', '2024-03-04 15:45:55.979802', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (142, 140, NULL, '新增', 'app:company:create', 2, '', 2, NULL, 1, 1, 1, '2024-03-04 15:46:11.260636', '2024-03-04 15:46:11.260636', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (143, 140, NULL, '更新', 'app:company:update', 2, '', 3, NULL, 1, 1, 1, '2024-03-04 15:46:25.098204', '2024-03-04 15:46:25.098204', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (144, 140, NULL, '删除', 'app:company:delete', 2, '', 4, NULL, 1, 1, 1, '2024-03-04 15:46:50.812446', '2024-03-04 15:46:50.812446', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (145, NULL, '/materials-inventory/product', '产品目录', 'app:product:list', 1, 'ant-design:product-outlined', 6, 'materials-inventory/product/index', 0, 1, 1, '2024-03-04 16:43:22.749281', '2024-03-27 12:56:52.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (146, 145, NULL, '单个查询', 'app:product:read', 2, '', 1, NULL, 1, 1, 1, '2024-03-04 16:44:56.482508', '2024-03-04 16:44:56.482508', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (147, 145, NULL, '新增', 'app:product:create', 2, '', 255, NULL, 1, 1, 1, '2024-03-04 16:45:08.211188', '2024-03-04 16:45:08.211188', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (148, 145, NULL, '更新', 'app:product:update', 2, '', 255, NULL, 1, 1, 1, '2024-03-04 16:45:25.457903', '2024-03-04 16:45:25.457903', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (149, 145, NULL, '删除', 'app:product:delete', 2, '', 255, NULL, 1, 1, 1, '2024-03-04 16:45:39.352621', '2024-03-04 16:45:39.352621', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (150, NULL, '/materials-inventory', '原材料盘点', NULL, 0, 'ant-design:dashboard-outlined', 3, NULL, 1, 0, 1, '2024-03-04 16:53:32.172674', '2024-04-02 14:52:58.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (151, 131, NULL, '导出', 'materials_inventory:history_in_out:export', 2, '', 5, NULL, 1, 1, 1, '2024-03-06 13:09:39.201093', '2024-03-06 13:09:39.201093', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (152, NULL, '/materials-inventory/inventory-check', '原材料库存管理', 'app:materials_inventory:list', 1, 'ant-design:dashboard-outlined', 2, 'materials-inventory/inventory-check/index', 1, 1, 1, '2024-03-06 13:33:24.795599', '2024-04-02 14:52:48.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (153, NULL, '/materials-inventory/project', '项目管理', 'app:project:list', 1, 'ep:memo', 4, 'materials-inventory/project/index', 0, 1, 1, '2024-03-07 09:28:19.234454', '2024-03-27 12:57:13.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (154, 153, NULL, '新增', 'app:project:create', 2, '', 1, NULL, 1, 1, 1, '2024-03-07 09:28:47.855064', '2024-03-07 09:28:47.855064', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (155, 153, NULL, '更新', 'app:project:update', 2, '', 2, NULL, 1, 1, 1, '2024-03-07 09:29:03.183084', '2024-03-07 09:29:03.183084', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (156, 153, NULL, '删除', 'app:project:delete', 2, '', 3, NULL, 1, 1, 1, '2024-03-07 09:29:16.684943', '2024-03-07 09:29:16.684943', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (157, 153, NULL, '单个信息', 'app:project:read', 2, '', 4, NULL, 1, 1, 1, '2024-03-07 09:29:33.424578', '2024-03-07 09:29:33.424578', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (158, 131, NULL, '导出原材料盘点表', 'app:materials_inventory:export', 2, '', 255, NULL, 1, 1, 0, '2024-03-07 11:46:54.468400', '2024-04-07 11:02:41.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (159, 130, NULL, '更新', 'app:vehicle_usage:update', 2, '', 255, NULL, 1, 1, 1, '2024-03-07 17:05:04.324327', '2024-03-07 17:05:04.324327', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (160, 130, NULL, '删除', 'app:vehicle_usage:delete', 2, '', 255, NULL, 1, 1, 1, '2024-03-07 17:05:13.776313', '2024-03-07 17:05:13.776313', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (161, 130, NULL, '新增', 'app:vehicle_usage:create', 2, '', 255, NULL, 1, 1, 1, '2024-03-07 17:05:25.081691', '2024-03-07 17:05:25.081691', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (162, 130, NULL, '单个信息', 'app:vehicle_usage:read', 2, '', 255, NULL, 1, 1, 1, '2024-03-07 17:05:48.310497', '2024-03-07 17:05:48.310497', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (163, 152, NULL, '导出', 'app:materials_inventory:export', 2, '', 255, NULL, 1, 1, 0, '2024-03-11 13:43:41.135585', '2024-04-07 11:02:08.000000', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (164, 152, NULL, '更新', 'app:materials_inventory:update', 2, '', 255, NULL, 1, 1, 1, '2024-03-11 13:44:23.144410', '2024-03-11 13:44:23.144410', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (165, 152, NULL, '删除', 'app:materials_inventory:delete', 2, '', 255, NULL, 1, 1, 1, '2024-03-11 13:44:47.383396', '2024-03-11 13:44:47.383396', 0, 1, NULL); +INSERT INTO `sys_menu` VALUES (166, 128, '/contract/task', '任务管控', NULL, 1, 'ant-design:align-left-outlined', 255, 'task/index', 0, 1, 1, '2024-03-12 10:18:54.645756', '2024-03-12 10:18:54.645756', 0, 1, NULL); + +-- ---------------------------- +-- Table structure for sys_role +-- ---------------------------- +DROP TABLE IF EXISTS `sys_role`; +CREATE TABLE `sys_role` ( + `id` int NOT NULL AUTO_INCREMENT, + `value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `status` tinyint NULL DEFAULT 1, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `default` tinyint NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `IDX_223de54d6badbe43a5490450c3`(`name`) USING BTREE, + UNIQUE INDEX `IDX_05edc0a51f41bb16b7d8137da9`(`value`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of sys_role +-- ---------------------------- +INSERT INTO `sys_role` VALUES (1, 'admin', '超级管理员', '超级管理员(拥有所有权限,请谨慎。)', 1, '2023-11-10 00:31:44.058463', '2024-04-07 11:08:14.419171', NULL); +INSERT INTO `sys_role` VALUES (2, 'user', '用户', '基础用户。目前没有设置任何菜单', 1, '2023-11-10 00:31:44.058463', '2024-04-07 11:05:32.000000', 1); +INSERT INTO `sys_role` VALUES (10, 'InventoryManager', '出入库管理员', '可以使用出入库相关的功能', 1, '2024-04-02 14:55:13.393542', '2024-04-07 11:09:03.195979', NULL); + +-- ---------------------------- +-- Table structure for sys_role_menus +-- ---------------------------- +DROP TABLE IF EXISTS `sys_role_menus`; +CREATE TABLE `sys_role_menus` ( + `role_id` int NOT NULL, + `menu_id` int NOT NULL, + PRIMARY KEY (`role_id`, `menu_id`) USING BTREE, + INDEX `IDX_35ce749b04d57e226d059e0f63`(`role_id`) USING BTREE, + INDEX `IDX_2b95fdc95b329d66c18f5baed6`(`menu_id`) USING BTREE, + CONSTRAINT `FK_2b95fdc95b329d66c18f5baed6d` FOREIGN KEY (`menu_id`) REFERENCES `sys_menu` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `FK_35ce749b04d57e226d059e0f633` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of sys_role_menus +-- ---------------------------- +INSERT INTO `sys_role_menus` VALUES (1, 43); +INSERT INTO `sys_role_menus` VALUES (2, 43); +INSERT INTO `sys_role_menus` VALUES (10, 48); +INSERT INTO `sys_role_menus` VALUES (10, 51); +INSERT INTO `sys_role_menus` VALUES (10, 52); +INSERT INTO `sys_role_menus` VALUES (10, 53); +INSERT INTO `sys_role_menus` VALUES (10, 131); +INSERT INTO `sys_role_menus` VALUES (10, 136); +INSERT INTO `sys_role_menus` VALUES (10, 137); +INSERT INTO `sys_role_menus` VALUES (10, 138); +INSERT INTO `sys_role_menus` VALUES (10, 139); +INSERT INTO `sys_role_menus` VALUES (10, 140); +INSERT INTO `sys_role_menus` VALUES (10, 141); +INSERT INTO `sys_role_menus` VALUES (10, 142); +INSERT INTO `sys_role_menus` VALUES (10, 143); +INSERT INTO `sys_role_menus` VALUES (10, 144); +INSERT INTO `sys_role_menus` VALUES (10, 145); +INSERT INTO `sys_role_menus` VALUES (10, 146); +INSERT INTO `sys_role_menus` VALUES (10, 147); +INSERT INTO `sys_role_menus` VALUES (10, 148); +INSERT INTO `sys_role_menus` VALUES (10, 149); +INSERT INTO `sys_role_menus` VALUES (10, 150); +INSERT INTO `sys_role_menus` VALUES (10, 151); +INSERT INTO `sys_role_menus` VALUES (10, 152); +INSERT INTO `sys_role_menus` VALUES (10, 153); +INSERT INTO `sys_role_menus` VALUES (10, 154); +INSERT INTO `sys_role_menus` VALUES (10, 155); +INSERT INTO `sys_role_menus` VALUES (10, 156); +INSERT INTO `sys_role_menus` VALUES (10, 157); +INSERT INTO `sys_role_menus` VALUES (10, 158); +INSERT INTO `sys_role_menus` VALUES (10, 163); +INSERT INTO `sys_role_menus` VALUES (10, 164); +INSERT INTO `sys_role_menus` VALUES (10, 165); + +-- ---------------------------- +-- Table structure for sys_task +-- ---------------------------- +DROP TABLE IF EXISTS `sys_task`; +CREATE TABLE `sys_task` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `service` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `type` tinyint NOT NULL DEFAULT 0, + `status` tinyint NOT NULL DEFAULT 1, + `start_time` datetime NULL DEFAULT NULL, + `end_time` datetime NULL DEFAULT NULL, + `limit` int NULL DEFAULT 0, + `cron` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `every` int NULL DEFAULT NULL, + `data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `job_opts` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `IDX_ef8e5ab5ef2fe0ddb1428439ef`(`name`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of sys_task +-- ---------------------------- +INSERT INTO `sys_task` VALUES (2, '定时清空登录日志', 'LogClearJob.clearLoginLog', 0, 1, NULL, NULL, 0, '0 0 3 ? * 1', 0, '', '{\"count\":1,\"key\":\"__default__:2:::0 0 3 ? * 1\",\"cron\":\"0 0 3 ? * 1\",\"jobId\":2}', '定时清空登录日志', '2023-11-10 00:31:44.197779', '2024-04-07 10:57:58.000000'); +INSERT INTO `sys_task` VALUES (3, '定时清空任务日志', 'LogClearJob.clearTaskLog', 0, 0, NULL, NULL, 0, '0 0 3 ? * 1', 0, '', '{\"count\":1,\"key\":\"__default__:3:::0 0 3 ? * 1\",\"cron\":\"0 0 3 ? * 1\",\"jobId\":3}', '定时清空任务日志', '2023-11-10 00:31:44.197779', '2024-03-22 14:12:52.000000'); +INSERT INTO `sys_task` VALUES (4, '访问百度首页', 'HttpRequestJob.handle', 0, 0, NULL, NULL, 1, '* * * * * ?', NULL, '{\"url\":\"https://www.baidu.com\",\"method\":\"get\"}', NULL, '访问百度首页', '2023-11-10 00:31:44.197779', '2023-11-10 00:31:44.206935'); +INSERT INTO `sys_task` VALUES (5, '发送邮箱', 'EmailJob.send', 0, 0, NULL, NULL, -1, '0 0 0 1 * ?', NULL, '{\"subject\":\"这是标题\",\"to\":\"18661983080@163.com\",\"content\":\"这是正文\"}', NULL, '每月发送邮箱', '2023-11-10 00:31:44.197779', '2024-03-07 11:14:53.000000'); + +-- ---------------------------- +-- Table structure for sys_task_log +-- ---------------------------- +DROP TABLE IF EXISTS `sys_task_log`; +CREATE TABLE `sys_task_log` ( + `id` int NOT NULL AUTO_INCREMENT, + `task_id` int NULL DEFAULT NULL, + `status` tinyint NOT NULL DEFAULT 0, + `detail` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `consume_time` int NULL DEFAULT 0, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`) USING BTREE, + INDEX `FK_f4d9c36052fdb188ff5c089454b`(`task_id`) USING BTREE, + CONSTRAINT `FK_f4d9c36052fdb188ff5c089454b` FOREIGN KEY (`task_id`) REFERENCES `sys_task` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of sys_task_log +-- ---------------------------- +INSERT INTO `sys_task_log` VALUES (1, 3, 1, NULL, 0, '2024-03-11 07:37:16.258223', '2024-03-11 07:37:16.258223'); +INSERT INTO `sys_task_log` VALUES (2, 2, 1, NULL, 0, '2024-03-11 08:29:25.175865', '2024-03-11 08:29:25.175865'); +INSERT INTO `sys_task_log` VALUES (3, 2, 1, NULL, 0, '2024-04-01 03:00:00.202419', '2024-04-01 03:00:00.202419'); + +-- ---------------------------- +-- Table structure for sys_user +-- ---------------------------- +DROP TABLE IF EXISTS `sys_user`; +CREATE TABLE `sys_user` ( + `id` int NOT NULL AUTO_INCREMENT, + `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `psalt` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `status` tinyint NULL DEFAULT 1, + `qq` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `dept_id` int NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `IDX_9e7164b2f1ea1348bc0eb0a7da`(`username`) USING BTREE, + INDEX `FK_96bde34263e2ae3b46f011124ac`(`dept_id`) USING BTREE, + CONSTRAINT `FK_96bde34263e2ae3b46f011124ac` FOREIGN KEY (`dept_id`) REFERENCES `sys_dept` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of sys_user +-- ---------------------------- +INSERT INTO `sys_user` VALUES (1, 'admin', 'a11571e778ee85e82caae2d980952546', 'https://thirdqq.qlogo.cn/g?b=qq&s=100&nk=1743369777', '1743369777@qq.com', '10086', '管理员', 'xQYCspvFb8cAW6GG1pOoUGTLqsuUSO3d', 1, '1743369777', '2023-11-10 00:31:44.104382', '2024-04-02 14:53:56.000000', '朱明仁', 2); +INSERT INTO `sys_user` VALUES (9, 'mengfei', '53a7b8157dbbbd51687c23cb783c5fe4', NULL, NULL, NULL, NULL, 'hm-VO0n2GO0qEZhEz6LdBBtFhIEMv0jo', 1, NULL, '2024-04-02 14:59:08.770239', '2024-04-07 10:50:24.000000', '孟菲', 2); +INSERT INTO `sys_user` VALUES (10, 'wangxinghao', '41c7f42c6e8a3eec7ac5b41ae0de84be', '[object Object]', NULL, NULL, NULL, '--IWP-ybu1ikzGpGOVCWEpkZ1hIheCVJ', 1, NULL, '2024-04-02 15:39:38.117227', '2024-04-07 11:07:49.000000', '王兴昊', 2); +INSERT INTO `sys_user` VALUES (11, 'zhangxueyong', 'c67a29c03f168650e2a43590328446b8', NULL, NULL, NULL, '张学勇', 'ucnRs7VTbXnwej2JQbKeYSoJ1gLy2Fwi', 1, NULL, '2024-04-03 08:10:08.192204', '2024-04-07 11:07:54.000000', '张学勇', 1); + +-- ---------------------------- +-- Table structure for sys_user_roles +-- ---------------------------- +DROP TABLE IF EXISTS `sys_user_roles`; +CREATE TABLE `sys_user_roles` ( + `user_id` int NOT NULL, + `role_id` int NOT NULL, + PRIMARY KEY (`user_id`, `role_id`) USING BTREE, + INDEX `IDX_96311d970191a044ec048011f4`(`user_id`) USING BTREE, + INDEX `IDX_6d61c5b3f76a3419d93a421669`(`role_id`) USING BTREE, + CONSTRAINT `FK_6d61c5b3f76a3419d93a4216695` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `FK_96311d970191a044ec048011f44` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of sys_user_roles +-- ---------------------------- +INSERT INTO `sys_user_roles` VALUES (1, 1); +INSERT INTO `sys_user_roles` VALUES (9, 10); +INSERT INTO `sys_user_roles` VALUES (10, 10); +INSERT INTO `sys_user_roles` VALUES (11, 10); + +-- ---------------------------- +-- Table structure for todo +-- ---------------------------- +DROP TABLE IF EXISTS `todo`; +CREATE TABLE `todo` ( + `id` int NOT NULL AUTO_INCREMENT, + `value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `user_id` int NULL DEFAULT NULL, + `status` tinyint NOT NULL DEFAULT 0, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`) USING BTREE, + INDEX `FK_9cb7989853c4cb7fe427db4b260`(`user_id`) USING BTREE, + CONSTRAINT `FK_9cb7989853c4cb7fe427db4b260` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of todo +-- ---------------------------- +INSERT INTO `todo` VALUES (1, 'nest.js', NULL, 0, '2023-11-10 00:31:44.139730', '2023-11-10 00:31:44.147629'); + +-- ---------------------------- +-- Table structure for tool_storage +-- ---------------------------- +DROP TABLE IF EXISTS `tool_storage`; +CREATE TABLE `tool_storage` ( + `id` int NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件名', + `fileName` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '真实文件名', + `ext_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + `size` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + `user_id` int NULL DEFAULT NULL, + `bussiness_module` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + `bussiness_record_id` int NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 280 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of tool_storage +-- ---------------------------- + +-- ---------------------------- +-- Table structure for user_access_tokens +-- ---------------------------- +DROP TABLE IF EXISTS `user_access_tokens`; +CREATE TABLE `user_access_tokens` ( + `id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `value` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `expired_at` datetime NOT NULL COMMENT '令牌过期时间', + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '令牌创建时间', + `user_id` int NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `FK_e9d9d0c303432e4e5e48c1c3e90`(`user_id`) USING BTREE, + CONSTRAINT `FK_e9d9d0c303432e4e5e48c1c3e90` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of user_access_tokens +-- ---------------------------- +INSERT INTO `user_access_tokens` VALUES ('067fa54e-e37d-45eb-850c-11fe6e580555', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInB2IjoxLCJyb2xlcyI6WyJhZG1pbiJdLCJpYXQiOjE3MTI0NTgyMTR9.KxpU61iQtg1j2zziHZB0gKGdvCiViIlTKkuY59EpD4s', '2024-04-08 10:50:14', '2024-04-07 10:50:14.399413', 1); +INSERT INTO `user_access_tokens` VALUES ('06fa487c-c9f4-4dcc-8643-ef6146e42add', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjksInB2IjoxLCJyb2xlcyI6WyJJbnZlbnRvcnlNYW5hZ2VyIl0sImlhdCI6MTcxMjQ1ODc5Mn0.mnEZ_Re_83_6NSgAf_E5Rp2akIOVsQQ_tQCgdk1QhZ0', '2024-04-08 10:59:53', '2024-04-07 10:59:52.570969', 9); +INSERT INTO `user_access_tokens` VALUES ('4b231512-42ac-41f6-9acc-9b3ac9590f12', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInB2IjoxLCJyb2xlcyI6WyJhZG1pbiJdLCJpYXQiOjE3MTI0NTY3MTd9._1XC-bHE_X0vWiCvexC5tXRIiZ5iwh6YX6DJO31KROk', '2024-04-08 10:25:18', '2024-04-07 10:25:17.962621', 1); +INSERT INTO `user_access_tokens` VALUES ('50be25ab-05a0-468f-919c-ec623ac41761', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInB2IjoxLCJyb2xlcyI6WyJhZG1pbiJdLCJpYXQiOjE3MTI0MTc2MjB9.otSnlbtKFKqoJUD5e0wM25Nxp6qjXs0r9FXnSO9zdOw', '2024-04-07 23:33:40', '2024-04-06 23:33:40.021878', 1); +INSERT INTO `user_access_tokens` VALUES ('64b831b0-32a9-4fad-ae3f-9f8f370f3bf2', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjksInB2IjoxLCJyb2xlcyI6WyJJbnZlbnRvcnlNYW5hZ2VyIl0sImlhdCI6MTcxMjQ1ODI3NH0.pmdigNgdbtUHrktkP8t4DhaxTKnBPogHUVdC5XEXSA4', '2024-04-08 10:51:14', '2024-04-07 10:51:14.116966', 9); +INSERT INTO `user_access_tokens` VALUES ('657d245b-b61f-4f84-9bb8-3358ad645546', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjksInB2IjoxLCJyb2xlcyI6WyJ1c2VyIiwiSW52ZW50b3J5TWFuYWdlciJdLCJpYXQiOjE3MTI0NTgxOTR9.BY5SGgYS7TjfVeCtRxVzgMZQq9TN9bMiOjDCoNwAcF0', '2024-04-08 10:49:55', '2024-04-07 10:49:54.981842', 9); +INSERT INTO `user_access_tokens` VALUES ('841bd4fc-b3e5-4864-ad4e-6849261e5806', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjksInB2IjoxLCJyb2xlcyI6WyJ1c2VyIiwiSW52ZW50b3J5TWFuYWdlciJdLCJpYXQiOjE3MTI0NTgxMzl9.fWCim-wvmY8b8HogIlThhsAeFf5oWYqT-evVpwv7-pc', '2024-04-08 10:48:59', '2024-04-07 10:48:59.360940', 9); +INSERT INTO `user_access_tokens` VALUES ('96ae5545-c90f-452a-bae7-377e8cf32169', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjksInB2IjoxLCJyb2xlcyI6WyJJbnZlbnRvcnlNYW5hZ2VyIl0sImlhdCI6MTcxMjQ1ODcyOH0.RuLjO4hFD_y1CoCQDB3KZJYS-24AWP1Q2vpEo_mVPxM', '2024-04-08 10:58:48', '2024-04-07 10:58:48.182856', 9); +INSERT INTO `user_access_tokens` VALUES ('a35d093a-c004-459c-a080-c0abc346a115', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInB2IjoxLCJyb2xlcyI6WyJhZG1pbiJdLCJpYXQiOjE3MTI0NTg5MTl9.JL6Xjpun6vX5XbruGhXLSevvO7AYLstCHSn_-z48ll8', '2024-04-08 11:02:00', '2024-04-07 11:01:59.632272', 1); +INSERT INTO `user_access_tokens` VALUES ('aa9787ae-7565-4d09-a3cb-b216947cfcab', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjksInB2IjoxLCJyb2xlcyI6WyJJbnZlbnRvcnlNYW5hZ2VyIl0sImlhdCI6MTcxMjQ1ODQwNX0.2CyoDt_tIsAzri02mhCKbVJEss-Ny2qdmZOPoZKUuxQ', '2024-04-08 10:53:25', '2024-04-07 10:53:25.379015', 9); +INSERT INTO `user_access_tokens` VALUES ('b6f1ce31-bf6b-4dc4-80eb-3ab37774e179', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInB2IjoxLCJyb2xlcyI6WyJhZG1pbiJdLCJpYXQiOjE3MTI0NTgzNDF9.bAvlmbaHNnL1_-ar9yVZI3Z_3Nm0qMF9MFN1bO29-rM', '2024-04-08 10:52:21', '2024-04-07 10:52:21.439166', 1); +INSERT INTO `user_access_tokens` VALUES ('c9e94b55-dd93-4ea1-aee1-c81d59a9e1bc', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjksInB2IjoxLCJyb2xlcyI6WyJJbnZlbnRvcnlNYW5hZ2VyIl0sImlhdCI6MTcxMjQ1ODgzOX0.ShvQOexZc7zfP_NsprmVjb0aXDqurp5_6Bsed0oJsq8', '2024-04-08 11:00:40', '2024-04-07 11:00:39.515606', 9); +INSERT INTO `user_access_tokens` VALUES ('c9ee2211-fa30-4a74-9e96-1622e959c5a5', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInB2IjoxLCJyb2xlcyI6WyJhZG1pbiJdLCJpYXQiOjE3MTI0NTkyNTB9.pX3TLszlGjQDyebNEXunGN6vwStARGHoOSLpULHfXWU', '2024-04-08 11:07:30', '2024-04-07 11:07:30.021625', 1); +INSERT INTO `user_access_tokens` VALUES ('cdb64fe0-ca48-466b-9caa-5812aa7a8634', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInB2IjoxLCJyb2xlcyI6WyJhZG1pbiJdLCJpYXQiOjE3MTI0NTc4MDZ9.jJ2SEjgLfYBJe80tV3CpD2WMMID8iAlCV_iJaaL6IXE', '2024-04-08 10:43:26', '2024-04-07 10:43:26.334882', 1); +INSERT INTO `user_access_tokens` VALUES ('d78ecadb-7bfd-4c65-a8c4-05e9473a80a5', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjksInB2IjoxLCJyb2xlcyI6WyJJbnZlbnRvcnlNYW5hZ2VyIl0sImlhdCI6MTcxMjQ1ODc1Nn0.HaB3ABe7S6Iik9lBEIa1ww_-QuxKsZOLkB8e4evcQ58', '2024-04-08 10:59:16', '2024-04-07 10:59:16.310195', 9); +INSERT INTO `user_access_tokens` VALUES ('d9f221ea-111d-47fe-932d-4123174dfce8', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInB2IjoxLCJyb2xlcyI6WyJhZG1pbiJdLCJpYXQiOjE3MTI0NTg2ODl9.N4Q7H8KnGd-hNmO-2UECuDfBFuuthrTPdATxDBcuWTI', '2024-04-08 10:58:10', '2024-04-07 10:58:09.590782', 1); +INSERT INTO `user_access_tokens` VALUES ('f886261e-bb1d-45db-83a4-71ef0f2a9180', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInB2IjoxLCJyb2xlcyI6WyJhZG1pbiJdLCJpYXQiOjE3MTI0NTg0NTF9.Uf2OXdWp1UgBHHw4dvLJcEQtv_09VoBAAsDkMmZ10Lo', '2024-04-08 10:54:11', '2024-04-07 10:54:11.390379', 1); + +-- ---------------------------- +-- Table structure for user_refresh_tokens +-- ---------------------------- +DROP TABLE IF EXISTS `user_refresh_tokens`; +CREATE TABLE `user_refresh_tokens` ( + `id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `value` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `expired_at` datetime NOT NULL COMMENT '令牌过期时间', + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '令牌创建时间', + `accessTokenId` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `REL_1dfd080c2abf42198691b60ae3`(`accessTokenId`) USING BTREE, + CONSTRAINT `FK_1dfd080c2abf42198691b60ae39` FOREIGN KEY (`accessTokenId`) REFERENCES `user_access_tokens` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of user_refresh_tokens +-- ---------------------------- +INSERT INTO `user_refresh_tokens` VALUES ('0aacf8f4-720a-4506-b6da-bf80ad7507ad', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoidGFNWHY3SzNIcnpGN0hPVm0wNTlZIiwiaWF0IjoxNzEyNDU5MjUwfQ.VuYA4gG5Cbuc6hSYAduwhi7t0qNmcHaSihF43dGnl_s', '2024-05-07 11:07:30', '2024-04-07 11:07:30.035958', 'c9ee2211-fa30-4a74-9e96-1622e959c5a5'); +INSERT INTO `user_refresh_tokens` VALUES ('1bbfc866-7019-4244-8fc5-ce3573c18f5a', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiZFUtMUZlS3lCNzZDY19NQ0JCTlVGIiwiaWF0IjoxNzEyNDU4ODM5fQ.hgp34zbgIzzU7nTYxjY-nqi44SeJzmAusZEFoBPFDxk', '2024-05-07 11:00:40', '2024-04-07 11:00:39.541546', 'c9e94b55-dd93-4ea1-aee1-c81d59a9e1bc'); +INSERT INTO `user_refresh_tokens` VALUES ('325d7565-b020-4de0-9c99-d645075678be', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiTDh1UV9XWEg3cXU5S2pRbUZGRWh3IiwiaWF0IjoxNzEyNDU4MjE0fQ.WHiXKLlA9p-Bjwud3EJ9sie1cpr5MImYuE7Vy3cwLjQ', '2024-05-07 10:50:14', '2024-04-07 10:50:14.409485', '067fa54e-e37d-45eb-850c-11fe6e580555'); +INSERT INTO `user_refresh_tokens` VALUES ('468da5b2-f67f-4d70-b043-7e479dc8d6a9', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoidXJCYUJKMkd0a0VfZGdOOC1hemh4IiwiaWF0IjoxNzEyNDU4OTE5fQ.Y2UvFoYF2i2rhzH12T_mggSi27pGk1jpX3WxqlRD6ho', '2024-05-07 11:02:00', '2024-04-07 11:01:59.643833', 'a35d093a-c004-459c-a080-c0abc346a115'); +INSERT INTO `user_refresh_tokens` VALUES ('4cd90a2b-ced3-455c-83a1-0b21b6fd5d8c', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiVnRqaHlEZnA2NURlMXh6WkxOYVl4IiwiaWF0IjoxNzEyNDU4MzQxfQ.RilIoFnJgeh-ze5Cy_ODvebySQyxIjJyIAoPB8S69Wc', '2024-05-07 10:52:21', '2024-04-07 10:52:21.451515', 'b6f1ce31-bf6b-4dc4-80eb-3ab37774e179'); +INSERT INTO `user_refresh_tokens` VALUES ('51c7573f-2d21-4206-a242-906d764a874d', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiTGROUjJ0blBZR1pEZXl4YTd6RXNGIiwiaWF0IjoxNzEyNDE3NjIwfQ.60gygI_tNscvCHdFI64XwRh9WUEWoFjscg-Q6Sl7EV8', '2024-05-06 23:33:40', '2024-04-06 23:33:40.032302', '50be25ab-05a0-468f-919c-ec623ac41761'); +INSERT INTO `user_refresh_tokens` VALUES ('7827a7fe-a694-4723-b11e-fd1fd542e81b', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiZDV6MXo5MjdMZ1V1bXJTUmY0WEloIiwiaWF0IjoxNzEyNDU4NzI4fQ.ViiYJc3DmYug_JbpcT4D6a65LVj23ebSkHuxWipFQkE', '2024-05-07 10:58:48', '2024-04-07 10:58:48.193052', '96ae5545-c90f-452a-bae7-377e8cf32169'); +INSERT INTO `user_refresh_tokens` VALUES ('7c504748-e2e6-41b6-a4b6-affc8bd8de72', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiajd1WmgxRjFmMnNqNWQ5Z0RienBwIiwiaWF0IjoxNzEyNDU4MTk0fQ.0BaG_gy41z-7y9EpZK05RV2-OgkDFHMEtJ9ofuVMLPs', '2024-05-07 10:49:55', '2024-04-07 10:49:54.993642', '657d245b-b61f-4f84-9bb8-3358ad645546'); +INSERT INTO `user_refresh_tokens` VALUES ('81c1b063-256a-4337-87db-e7bb01a9c793', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiOThjSTdkSkJQZGdFZ3lJQjJRODI3IiwiaWF0IjoxNzEyNDU4MTM5fQ.fpDITw5fInmn4_fXde-O-i-SgV_9-lGrAqk5Rq-v1VY', '2024-05-07 10:48:59', '2024-04-07 10:48:59.389990', '841bd4fc-b3e5-4864-ad4e-6849261e5806'); +INSERT INTO `user_refresh_tokens` VALUES ('879409aa-4e60-491f-b155-cb4433832ae8', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiUnJBMFRQbGtnckpIRTNUVG83QjRPIiwiaWF0IjoxNzEyNDU4NzkyfQ.YFufOp1ytb6ZyzaMFscf972VMIaPb09GsG9Z0FBp7R8', '2024-05-07 10:59:53', '2024-04-07 10:59:52.580656', '06fa487c-c9f4-4dcc-8643-ef6146e42add'); +INSERT INTO `user_refresh_tokens` VALUES ('89b9df53-5d1b-4640-878c-821db4ddafe1', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiVkJlOEZ5OHJ3Um1HQkxfMVRNQzNlIiwiaWF0IjoxNzEyNDU4Njg5fQ.SwXWgzeLNKAvQU39SHrQYrGSQmYWAaAjOF0WVNveHGc', '2024-05-07 10:58:10', '2024-04-07 10:58:09.604051', 'd9f221ea-111d-47fe-932d-4123174dfce8'); +INSERT INTO `user_refresh_tokens` VALUES ('8ba5e525-a9eb-4823-b41e-ef9fb04624a2', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiSVY5bW1WRDRzY0xwaE00ek90ZmRaIiwiaWF0IjoxNzEyNDU3ODA2fQ.UKJaAJvXJJg96almY9QNb8mrjyBPewQvw5ECgZGjRpI', '2024-05-07 10:43:26', '2024-04-07 10:43:26.350399', 'cdb64fe0-ca48-466b-9caa-5812aa7a8634'); +INSERT INTO `user_refresh_tokens` VALUES ('ab50f093-429b-4fb3-b1d4-f91e3205419a', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiT0IwbzVHVFkxazNERVF0eFU4N0RGIiwiaWF0IjoxNzEyNDU4NzU2fQ.I9miRO1z2X_Uhf8QJxcjk6gKOisvrVISMZY3nqxUE1g', '2024-05-07 10:59:16', '2024-04-07 10:59:16.320141', 'd78ecadb-7bfd-4c65-a8c4-05e9473a80a5'); +INSERT INTO `user_refresh_tokens` VALUES ('b5913f11-9132-4342-a5fc-1823b4f8095e', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiRHo3cmRRck5nYkRGYTNra3lYZkY4IiwiaWF0IjoxNzEyNDU4NDUxfQ.ITzqgmr_hZo7yoaKeoYa7DCV51EHGSSW95aJOgHASqc', '2024-05-07 10:54:11', '2024-04-07 10:54:11.402462', 'f886261e-bb1d-45db-83a4-71ef0f2a9180'); +INSERT INTO `user_refresh_tokens` VALUES ('c996e647-0b40-4271-89a4-18c6fa361648', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoibTd5VkxYUDRSWndNYTRBOE5qTk9CIiwiaWF0IjoxNzEyNDU4NDA1fQ.ImGnCiah6WX6tHltPV-xphw7i-CHP2IU-BwOElc2uy4', '2024-05-07 10:53:25', '2024-04-07 10:53:25.390342', 'aa9787ae-7565-4d09-a3cb-b216947cfcab'); +INSERT INTO `user_refresh_tokens` VALUES ('ce1d039d-ed44-41f6-9228-06720e288088', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoicVZPU0xfd3hwRnZjRi1fNVdwVGZqIiwiaWF0IjoxNzEyNDU2NzE3fQ.u7EV8m_sg7svflMYEitqvkOgXnOlKG-pLjD6fQ4Hnc8', '2024-05-07 10:25:18', '2024-04-07 10:25:17.983231', '4b231512-42ac-41f6-9acc-9b3ac9590f12'); +INSERT INTO `user_refresh_tokens` VALUES ('ff56df48-89bb-46b3-a8d2-80426dba6e43', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoialZYS3JNUVZUMC1PbDJYMXJBZmE1IiwiaWF0IjoxNzEyNDU4Mjc0fQ.l24dDsJs64KhV9SPzltTGl-8o75aqYXTczGtgieJ8H8', '2024-05-07 10:51:14', '2024-04-07 10:51:14.126506', '64b831b0-32a9-4fad-ae3f-9f8f370f3bf2'); + +-- ---------------------------- +-- Table structure for vehicle_usage +-- ---------------------------- +DROP TABLE IF EXISTS `vehicle_usage`; +CREATE TABLE `vehicle_usage` ( + `id` int NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `reviewer` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '审核人', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '审核状态(字典)', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `year` int NOT NULL COMMENT '年度', + `vehicle_id` int NOT NULL COMMENT '外出使用的车辆名称(字典)', + `applicant` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '申请人', + `driver` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '出行司机', + `current_mileage` int NULL DEFAULT NULL COMMENT '当前车辆里程数(KM)', + `expected_start_date` date NULL DEFAULT NULL COMMENT '预计出行开始时间', + `expected_end_date` date NULL DEFAULT NULL COMMENT '预计出行结束时间', + `purpose` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '使用事由', + `actual_return_time` date NULL DEFAULT NULL COMMENT '实际回司时间', + `return_mileage` int NULL DEFAULT NULL COMMENT '回城车辆里程数(KM)', + `partner` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '随行人员', + PRIMARY KEY (`id`) USING BTREE, + INDEX `FK_6aff0ec40ff474e6228c1125f5c`(`vehicle_id`) USING BTREE, + CONSTRAINT `FK_6aff0ec40ff474e6228c1125f5c` FOREIGN KEY (`vehicle_id`) REFERENCES `sys_dict_item` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of vehicle_usage +-- ---------------------------- + +-- ---------------------------- +-- Table structure for vehicle_usage_storage +-- ---------------------------- +DROP TABLE IF EXISTS `vehicle_usage_storage`; +CREATE TABLE `vehicle_usage_storage` ( + `vehicle_id` int NOT NULL, + `file_id` int NOT NULL, + PRIMARY KEY (`vehicle_id`, `file_id`) USING BTREE, + INDEX `IDX_1d122393de1ee773c383569e71`(`vehicle_id`) USING BTREE, + INDEX `IDX_a8cbcb6835a9212dd2a49b50ed`(`file_id`) USING BTREE, + CONSTRAINT `FK_1d122393de1ee773c383569e717` FOREIGN KEY (`vehicle_id`) REFERENCES `vehicle_usage` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `FK_a8cbcb6835a9212dd2a49b50ed9` FOREIGN KEY (`file_id`) REFERENCES `tool_storage` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of vehicle_usage_storage +-- ---------------------------- + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/minio.js b/minio.js new file mode 100644 index 0000000..8d0410f --- /dev/null +++ b/minio.js @@ -0,0 +1,14 @@ +const Minio = require('minio'); + +const minioClient = new Minio.Client({ + endPoint: '144.123.43.138', + port: 8021, + useSSL: false, + accessKey: '8Zttvx4ZbF2ikFRb', + secretKey: 'SCgOJEJXM5vMNQL4fF8opXA1wmpACRfw' +}); + +minioClient.listBuckets((err, buckets) => { + if (err) return console.log(err); + console.log('Buckets:', buckets); +}); diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 0000000..86963b2 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true, + "assets": [ + { "include": "assets/**/*", "outDir": "dist", "watchAssets": true } + ], + "plugins": [{ + "name": "@nestjs/swagger", + "options": { + "introspectComments": true + } + }] + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..874abc1 --- /dev/null +++ b/package.json @@ -0,0 +1,176 @@ +{ + "name": "huaxin-admin", + "version": "2.0.0", + "private": true, + "packageManager": "pnpm@8.10.2", + "license": "MIT", + "engines": { + "node": ">=18", + "pnpm": ">=8.1.0" + }, + "scripts": { + "postinstall": "npm run gen-env-types", + "prebuild": "rimraf dist", + "build": "nest build", + "dev": "npm run start", + "dev:debug": "npm run start:debug", + "repl": "npm run start -- --entryFile repl", + "bundle": "rimraf out && npm run build && ncc build dist/main.js -o out -m -t && chmod +x out/index.js", + "start": "cross-env NODE_ENV=development nest start -w --path tsconfig.json", + "start:debug": "cross-env NODE_ENV=development nest start --debug --watch", + "start:prod": "cross-env NODE_ENV=production node dist/main", + "prod": "cross-env NODE_ENV=production pm2-runtime start ecosystem.config.js", + "prod:pm2": "cross-env NODE_ENV=production pm2 restart ecosystem.config.js", + "prod:stop": "pm2 stop ecosystem.config.js", + "prod:debug": "cross-env NODE_ENV=production nest start --debug --watch", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "doc": "compodoc -p tsconfig.json -s", + "gen-env-types": "npx tsx scripts/genEnvTypes.ts", + "typeorm": "NODE_ENV=development typeorm-ts-node-esm -d ./dist/config/database.config.js", + "migration:create": "npm run typeorm migration:create ./src/migrations/initData", + "migration:generate": "npm run typeorm migration:generate ./src/migrations/update-table_$(echo $npm_package_version | sed 's/\\./_/g')", + "migration:run": "npm run typeorm -- migration:run", + "migration:revert": "npm run typeorm -- migration:revert", + "cleanlog": "rimraf logs", + "docker:build:dev": "docker compose --env-file .env --env-file .env.development up --build", + "docker:build": "docker compose --env-file .env --env-file .env.production up --build", + "docker:up": "docker compose --env-file .env --env-file .env.production up -d --no-build", + "docker:down": "docker compose --env-file .env --env-file .env.production down", + "docker:rmi": "docker compose --env-file .env --env-file .env.production stop huaxin-admin-server && docker container rm huaxin-admin-server && docker rmi huaxin-admin-server", + "docker:logs": "docker compose --env-file .env --env-file .env.production logs -f", + "c": "git add . && git cz && git push", + "release": "standard-version", + "commitlint": "commitlint --config commitlint.config.cjs -e -V", + "format": "prettier --write \"src/**/*.ts\"" + }, + "dependencies": { + "@fastify/cookie": "^9.3.1", + "@fastify/multipart": "^8.1.0", + "@fastify/static": "^7.0.1", + "@liaoliaots/nestjs-redis": "^9.0.5", + "@nestjs-modules/mailer": "^1.10.3", + "@nestjs/axios": "^3.0.2", + "@nestjs/bull": "^10.1.0", + "@nestjs/cache-manager": "^2.2.1", + "@nestjs/common": "^10.3.3", + "@nestjs/config": "^3.2.0", + "@nestjs/core": "^10.3.3", + "@nestjs/event-emitter": "^2.0.4", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-fastify": "^10.3.3", + "@nestjs/platform-socket.io": "^10.3.3", + "@nestjs/schedule": "^4.0.1", + "@nestjs/swagger": "^7.3.0", + "@nestjs/terminus": "^10.2.2", + "@nestjs/throttler": "^5.1.2", + "@nestjs/typeorm": "^10.0.2", + "@nestjs/websockets": "^10.3.3", + "@socket.io/redis-adapter": "^8.2.1", + "@socket.io/redis-emitter": "^5.1.0", + "@types/lodash": "^4.14.202", + "axios": "^1.6.7", + "bull": "^4.12.2", + "cache-manager": "^5.4.0", + "cache-manager-ioredis-yet": "^1.2.2", + "chalk": "^5.3.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "cron": "^3.1.6", + "cron-parser": "^4.9.0", + "crypto-js": "^4.2.0", + "dayjs": "^1.11.10", + "dotenv": "16.4.4", + "exceljs": "^4.4.0", + "handlebars": "^4.7.8", + "helmet": "^7.1.0", + "ioredis": "^5.3.2", + "lodash": "^4.17.21", + "mathjs": "^12.4.0", + "mysql2": "^3.9.1", + "nanoid": "^3.3.7", + "nestjs-minio": "^2.5.4", + "nodemailer": "^6.9.9", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", + "pinyin": "3", + "qiniu": "^7.11.0", + "reflect-metadata": "^0.2.1", + "rimraf": "^5.0.5", + "rxjs": "^7.8.1", + "socket.io": "^4.7.4", + "stacktrace-js": "^2.0.2", + "svg-captcha": "^1.4.0", + "systeminformation": "^5.22.0", + "typeorm": "0.3.17", + "ua-parser-js": "^1.0.37", + "winston": "^3.11.0", + "winston-daily-rotate-file": "^5.0.0" + }, + "devDependencies": { + "@compodoc/compodoc": "^1.1.23", + "@nestjs/cli": "^10.3.2", + "@nestjs/schematics": "^10.1.1", + "@nestjs/testing": "^10.3.2", + "@types/cache-manager": "^4.0.6", + "@types/jest": "29.5.12", + "@types/multer": "^1.4.11", + "@types/node": "^20.11.16", + "@types/supertest": "^6.0.2", + "@types/ua-parser-js": "^0.7.39", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "cliui": "^8.0.1", + "commitizen": "^4.3.0", + "cross-env": "^7.0.3", + "cz-customizable": "^7.0.0", + "eslint": "^8.0.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^4.0.0", + "husky": "^8.0.0", + "jest": "^29.7.0", + "prettier": "~3.2.5", + "source-map-support": "^0.5.21", + "standard-version": "^9.5.0", + "supertest": "^6.3.4", + "ts-jest": "^29.1.2", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.3.3" + }, + "config": { + "commitizen": { + "path": "cz-customizable" + } + }, + "lint-staged": { + "*": [ + "npm run lint" + ] + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "moduleNameMapper": { + "^~/(.*)$": "/$1" + }, + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..ee995eb --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,12671 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@fastify/cookie': + specifier: ^9.3.1 + version: 9.3.1 + '@fastify/multipart': + specifier: ^8.1.0 + version: 8.1.0 + '@fastify/static': + specifier: ^7.0.1 + version: 7.0.1 + '@liaoliaots/nestjs-redis': + specifier: ^9.0.5 + version: 9.0.5(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(ioredis@5.3.2) + '@nestjs-modules/mailer': + specifier: ^1.10.3 + version: 1.10.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(nodemailer@6.9.9) + '@nestjs/axios': + specifier: ^3.0.2 + version: 3.0.2(@nestjs/common@10.3.3)(axios@1.6.7)(rxjs@7.8.1) + '@nestjs/bull': + specifier: ^10.1.0 + version: 10.1.0(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(bull@4.12.2) + '@nestjs/cache-manager': + specifier: ^2.2.1 + version: 2.2.1(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(cache-manager@5.4.0)(rxjs@7.8.1) + '@nestjs/common': + specifier: ^10.3.3 + version: 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/config': + specifier: ^3.2.0 + version: 3.2.0(@nestjs/common@10.3.3)(rxjs@7.8.1) + '@nestjs/core': + specifier: ^10.3.3 + version: 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/event-emitter': + specifier: ^2.0.4 + version: 2.0.4(@nestjs/common@10.3.3)(@nestjs/core@10.3.3) + '@nestjs/jwt': + specifier: ^10.2.0 + version: 10.2.0(@nestjs/common@10.3.3) + '@nestjs/passport': + specifier: ^10.0.3 + version: 10.0.3(@nestjs/common@10.3.3)(passport@0.7.0) + '@nestjs/platform-fastify': + specifier: ^10.3.3 + version: 10.3.3(@fastify/static@7.0.1)(@nestjs/common@10.3.3)(@nestjs/core@10.3.3) + '@nestjs/platform-socket.io': + specifier: ^10.3.3 + version: 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(rxjs@7.8.1) + '@nestjs/schedule': + specifier: ^4.0.1 + version: 4.0.1(@nestjs/common@10.3.3)(@nestjs/core@10.3.3) + '@nestjs/swagger': + specifier: ^7.3.0 + version: 7.3.0(@fastify/static@7.0.1)(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1) + '@nestjs/terminus': + specifier: ^10.2.2 + version: 10.2.2(@nestjs/axios@3.0.2)(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(@nestjs/typeorm@10.0.2)(reflect-metadata@0.2.1)(rxjs@7.8.1)(typeorm@0.3.17) + '@nestjs/throttler': + specifier: ^5.1.2 + version: 5.1.2(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(reflect-metadata@0.2.1) + '@nestjs/typeorm': + specifier: ^10.0.2 + version: 10.0.2(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1)(typeorm@0.3.17) + '@nestjs/websockets': + specifier: ^10.3.3 + version: 10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(@nestjs/platform-socket.io@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@socket.io/redis-adapter': + specifier: ^8.2.1 + version: 8.2.1(socket.io-adapter@2.5.2) + '@socket.io/redis-emitter': + specifier: ^5.1.0 + version: 5.1.0 + '@types/lodash': + specifier: ^4.14.202 + version: 4.14.202 + axios: + specifier: ^1.6.7 + version: 1.6.7 + bull: + specifier: ^4.12.2 + version: 4.12.2 + cache-manager: + specifier: ^5.4.0 + version: 5.4.0 + cache-manager-ioredis-yet: + specifier: ^1.2.2 + version: 1.2.2 + chalk: + specifier: ^5.3.0 + version: 5.3.0 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.1 + version: 0.14.1 + cron: + specifier: ^3.1.6 + version: 3.1.6 + cron-parser: + specifier: ^4.9.0 + version: 4.9.0 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + dayjs: + specifier: ^1.11.10 + version: 1.11.10 + dotenv: + specifier: 16.4.4 + version: 16.4.4 + exceljs: + specifier: ^4.4.0 + version: 4.4.0 + handlebars: + specifier: ^4.7.8 + version: 4.7.8 + helmet: + specifier: ^7.1.0 + version: 7.1.0 + ioredis: + specifier: ^5.3.2 + version: 5.3.2 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + mathjs: + specifier: ^12.4.0 + version: 12.4.0 + mysql2: + specifier: ^3.9.1 + version: 3.9.1 + nanoid: + specifier: ^3.3.7 + version: 3.3.7 + nestjs-minio: + specifier: ^2.5.4 + version: 2.5.4(@nestjs/common@10.3.3)(@nestjs/core@10.3.3) + nodemailer: + specifier: ^6.9.9 + version: 6.9.9 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-google-oauth20: + specifier: ^2.0.0 + version: 2.0.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + passport-local: + specifier: ^1.0.0 + version: 1.0.0 + pinyin: + specifier: '3' + version: 3.1.0 + qiniu: + specifier: ^7.11.0 + version: 7.11.0 + reflect-metadata: + specifier: ^0.2.1 + version: 0.2.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.5 + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + socket.io: + specifier: ^4.7.4 + version: 4.7.4 + stacktrace-js: + specifier: ^2.0.2 + version: 2.0.2 + svg-captcha: + specifier: ^1.4.0 + version: 1.4.0 + systeminformation: + specifier: ^5.22.0 + version: 5.22.0 + typeorm: + specifier: 0.3.17 + version: 0.3.17(ioredis@5.3.2)(mysql2@3.9.1)(ts-node@10.9.2) + ua-parser-js: + specifier: ^1.0.37 + version: 1.0.37 + winston: + specifier: ^3.11.0 + version: 3.11.0 + winston-daily-rotate-file: + specifier: ^5.0.0 + version: 5.0.0(winston@3.11.0) + +devDependencies: + '@compodoc/compodoc': + specifier: ^1.1.23 + version: 1.1.23(typescript@5.3.3) + '@nestjs/cli': + specifier: ^10.3.2 + version: 10.3.2 + '@nestjs/schematics': + specifier: ^10.1.1 + version: 10.1.1(chokidar@3.6.0)(typescript@5.3.3) + '@nestjs/testing': + specifier: ^10.3.2 + version: 10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3) + '@types/cache-manager': + specifier: ^4.0.6 + version: 4.0.6 + '@types/jest': + specifier: 29.5.12 + version: 29.5.12 + '@types/multer': + specifier: ^1.4.11 + version: 1.4.11 + '@types/node': + specifier: ^20.11.16 + version: 20.11.18 + '@types/supertest': + specifier: ^6.0.2 + version: 6.0.2 + '@types/ua-parser-js': + specifier: ^0.7.39 + version: 0.7.39 + '@typescript-eslint/eslint-plugin': + specifier: ^5.0.0 + version: 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/parser': + specifier: ^5.0.0 + version: 5.62.0(eslint@8.57.0)(typescript@5.3.3) + cliui: + specifier: ^8.0.1 + version: 8.0.1 + commitizen: + specifier: ^4.3.0 + version: 4.3.0(@types/node@20.11.18)(typescript@5.3.3) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + cz-customizable: + specifier: ^7.0.0 + version: 7.0.0 + eslint: + specifier: ^8.0.1 + version: 8.57.0 + eslint-config-prettier: + specifier: ^8.3.0 + version: 8.10.0(eslint@8.57.0) + eslint-plugin-prettier: + specifier: ^4.0.0 + version: 4.2.1(eslint-config-prettier@8.10.0)(eslint@8.57.0)(prettier@3.2.5) + husky: + specifier: ^8.0.0 + version: 8.0.3 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.11.18)(ts-node@10.9.2) + prettier: + specifier: ~3.2.5 + version: 3.2.5 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + standard-version: + specifier: ^9.5.0 + version: 9.5.0 + supertest: + specifier: ^6.3.4 + version: 6.3.4 + ts-jest: + specifier: ^29.1.2 + version: 29.1.2(@babel/core@7.23.9)(jest@29.7.0)(typescript@5.3.3) + ts-loader: + specifier: ^9.5.1 + version: 9.5.1(typescript@5.3.3)(webpack@5.90.1) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.11.18)(typescript@5.3.3) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + typescript: + specifier: ^5.3.3 + version: 5.3.3 + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@aduh95/viz.js@3.4.0: + resolution: {integrity: sha512-KI2nVf9JdwWCXqK6RVf+9/096G7VWN4Z84mnynlyZKao2xQENW8WNEjLmvdlxS5X8PNWXFC1zqwm7tveOXw/4A==} + dev: true + + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.22 + dev: true + + /@angular-devkit/core@14.2.12(chokidar@3.6.0): + resolution: {integrity: sha512-tg1+deEZdm3fgk2BQ6y7tujciL6qhtN5Ums266lX//kAZeZ4nNNXTBT+oY5xgfjvmLbW+xKg0XZrAS0oIRKY5g==} + engines: {node: ^14.15.0 || >=16.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^3.5.2 + peerDependenciesMeta: + chokidar: + optional: true + dependencies: + ajv: 8.11.0 + ajv-formats: 2.1.1(ajv@8.11.0) + chokidar: 3.6.0 + jsonc-parser: 3.1.0 + rxjs: 6.6.7 + source-map: 0.7.4 + dev: true + + /@angular-devkit/core@17.1.2(chokidar@3.6.0): + resolution: {integrity: sha512-ku+/W/HMCBacSWFppenr9y6Lx8mDuTuQvn1IkTyBLiJOpWnzgVbx9kHDeaDchGa1PwLlJUBBrv27t3qgJOIDPw==} + engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^3.5.2 + peerDependenciesMeta: + chokidar: + optional: true + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + chokidar: 3.6.0 + jsonc-parser: 3.2.0 + picomatch: 3.0.1 + rxjs: 7.8.1 + source-map: 0.7.4 + dev: true + + /@angular-devkit/schematics-cli@17.1.2(chokidar@3.6.0): + resolution: {integrity: sha512-bvXykYzSST05qFdlgIzUguNOb3z0hCa8HaTwtqdmQo9aFPf+P+/AC56I64t1iTchMjQtf3JrBQhYM25gUdcGbg==} + engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + dependencies: + '@angular-devkit/core': 17.1.2(chokidar@3.6.0) + '@angular-devkit/schematics': 17.1.2(chokidar@3.6.0) + ansi-colors: 4.1.3 + inquirer: 9.2.12 + symbol-observable: 4.0.0 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - chokidar + dev: true + + /@angular-devkit/schematics@14.2.12(chokidar@3.6.0): + resolution: {integrity: sha512-MN5yGR+SSSPPBBVMf4cifDJn9u0IYvxiHst+HWokH2AkBYy+vB1x8jYES2l1wkiISD7nvjTixfqX+Y95oMBoLg==} + engines: {node: ^14.15.0 || >=16.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + dependencies: + '@angular-devkit/core': 14.2.12(chokidar@3.6.0) + jsonc-parser: 3.1.0 + magic-string: 0.26.2 + ora: 5.4.1 + rxjs: 6.6.7 + transitivePeerDependencies: + - chokidar + dev: true + + /@angular-devkit/schematics@17.1.2(chokidar@3.6.0): + resolution: {integrity: sha512-8S9RuM8olFN/gwN+mjbuF1CwHX61f0i59EGXz9tXLnKRUTjsRR+8vVMTAmX0dvVAT5fJTG/T69X+HX7FeumdqA==} + engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + dependencies: + '@angular-devkit/core': 17.1.2(chokidar@3.6.0) + jsonc-parser: 3.2.0 + magic-string: 0.30.5 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + dev: true + + /@babel/code-frame@7.23.5: + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.23.4 + chalk: 2.4.2 + + /@babel/compat-data@7.23.5: + resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.23.9: + resolution: {integrity: sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.9) + '@babel/helpers': 7.23.9 + '@babel/parser': 7.23.9 + '@babel/template': 7.23.9 + '@babel/traverse': 7.23.9 + '@babel/types': 7.23.9 + convert-source-map: 2.0.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.23.6: + resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.9 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.22 + jsesc: 2.5.2 + dev: true + + /@babel/helper-annotate-as-pure@7.22.5: + resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@babel/helper-builder-binary-assignment-operator-visitor@7.22.15: + resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@babel/helper-compilation-targets@7.23.6: + resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.23.5 + '@babel/helper-validator-option': 7.23.5 + browserslist: 4.23.0 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-create-class-features-plugin@7.23.10(@babel/core@7.23.9): + resolution: {integrity: sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.9) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + semver: 6.3.1 + dev: true + + /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.23.9): + resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-annotate-as-pure': 7.22.5 + regexpu-core: 5.3.2 + semver: 6.3.1 + dev: true + + /@babel/helper-define-polyfill-provider@0.5.0(@babel/core@7.23.9): + resolution: {integrity: sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + debug: 4.3.4 + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.23.9 + '@babel/types': 7.23.9 + dev: true + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@babel/helper-member-expression-to-functions@7.23.0: + resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@babel/helper-module-imports@7.22.15: + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + + /@babel/helper-optimise-call-expression@7.22.5: + resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@babel/helper-plugin-utils@7.22.5: + resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.23.9): + resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-wrap-function': 7.22.20 + dev: true + + /@babel/helper-replace-supers@7.22.20(@babel/core@7.23.9): + resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + dev: true + + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@babel/helper-skip-transparent-expression-wrappers@7.22.5: + resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-option@7.23.5: + resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-wrap-function@7.22.20: + resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-function-name': 7.23.0 + '@babel/template': 7.23.9 + '@babel/types': 7.23.9 + dev: true + + /@babel/helpers@7.23.9: + resolution: {integrity: sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.23.9 + '@babel/traverse': 7.23.9 + '@babel/types': 7.23.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight@7.23.4: + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + + /@babel/parser@7.23.9: + resolution: {integrity: sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.9 + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.23.9) + dev: true + + /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.7(@babel/core@7.23.9): + resolution: {integrity: sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.23.9): + resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-create-class-features-plugin': 7.23.10(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.9): + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + dev: true + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.9): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.9): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.9): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.9): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.23.9): + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.23.9): + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.9): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.9): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.9): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.9): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.9): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.9): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.9): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.9): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.9): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.9): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.23.9): + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-async-generator-functions@7.23.9(@babel/core@7.23.9): + resolution: {integrity: sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.9) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.9) + dev: true + + /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.9) + dev: true + + /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-block-scoping@7.23.4(@babel/core@7.23.9): + resolution: {integrity: sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-create-class-features-plugin': 7.23.10(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-class-static-block@7.23.4(@babel/core@7.23.9): + resolution: {integrity: sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-create-class-features-plugin': 7.23.10(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.9) + dev: true + + /@babel/plugin-transform-classes@7.23.8(@babel/core@7.23.9): + resolution: {integrity: sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.9) + '@babel/helper-split-export-declaration': 7.22.6 + globals: 11.12.0 + dev: true + + /@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/template': 7.23.9 + dev: true + + /@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-duplicate-keys@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-dynamic-import@7.23.4(@babel/core@7.23.9): + resolution: {integrity: sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.9) + dev: true + + /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-export-namespace-from@7.23.4(@babel/core@7.23.9): + resolution: {integrity: sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.9) + dev: true + + /@babel/plugin-transform-for-of@7.23.6(@babel/core@7.23.9): + resolution: {integrity: sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + dev: true + + /@babel/plugin-transform-function-name@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-json-strings@7.23.4(@babel/core@7.23.9): + resolution: {integrity: sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.9) + dev: true + + /@babel/plugin-transform-literals@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-logical-assignment-operators@7.23.4(@babel/core@7.23.9): + resolution: {integrity: sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.9) + dev: true + + /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-systemjs@7.23.9(@babel/core@7.23.9): + resolution: {integrity: sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + + /@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.23.9): + resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-new-target@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-nullish-coalescing-operator@7.23.4(@babel/core@7.23.9): + resolution: {integrity: sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.9) + dev: true + + /@babel/plugin-transform-numeric-separator@7.23.4(@babel/core@7.23.9): + resolution: {integrity: sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.9) + dev: true + + /@babel/plugin-transform-object-rest-spread@7.23.4(@babel/core@7.23.9): + resolution: {integrity: sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.23.5 + '@babel/core': 7.23.9 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.9) + dev: true + + /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.9) + dev: true + + /@babel/plugin-transform-optional-catch-binding@7.23.4(@babel/core@7.23.9): + resolution: {integrity: sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.9) + dev: true + + /@babel/plugin-transform-optional-chaining@7.23.4(@babel/core@7.23.9): + resolution: {integrity: sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.9) + dev: true + + /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-create-class-features-plugin': 7.23.10(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-private-property-in-object@7.23.4(@babel/core@7.23.9): + resolution: {integrity: sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-create-class-features-plugin': 7.23.10(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.9) + dev: true + + /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + regenerator-transform: 0.15.2 + dev: true + + /@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-spread@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + dev: true + + /@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.9) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/preset-env@7.23.9(@babel/core@7.23.9): + resolution: {integrity: sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.23.5 + '@babel/core': 7.23.9 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.23.5 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.7(@babel/core@7.23.9) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.9) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.9) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.9) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.9) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.9) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.9) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.9) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.9) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.9) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.23.9) + '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-async-generator-functions': 7.23.9(@babel/core@7.23.9) + '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-block-scoping': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-class-static-block': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-classes': 7.23.8(@babel/core@7.23.9) + '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-dynamic-import': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-export-namespace-from': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-for-of': 7.23.6(@babel/core@7.23.9) + '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-json-strings': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-logical-assignment-operators': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-modules-systemjs': 7.23.9(@babel/core@7.23.9) + '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.23.9) + '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-nullish-coalescing-operator': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-numeric-separator': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-object-rest-spread': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-optional-catch-binding': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-private-property-in-object': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.23.9) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.23.9) + babel-plugin-polyfill-corejs2: 0.4.8(@babel/core@7.23.9) + babel-plugin-polyfill-corejs3: 0.9.0(@babel/core@7.23.9) + babel-plugin-polyfill-regenerator: 0.5.5(@babel/core@7.23.9) + core-js-compat: 3.36.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.23.9): + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/types': 7.23.9 + esutils: 2.0.3 + dev: true + + /@babel/regjsgen@0.8.0: + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} + dev: true + + /@babel/runtime@7.23.9: + resolution: {integrity: sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + + /@babel/template@7.23.9: + resolution: {integrity: sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 + dev: true + + /@babel/traverse@7.23.9: + resolution: {integrity: sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.23.9: + resolution: {integrity: sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.23.4 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + + /@colors/colors@1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + requiresBuild: true + dev: true + optional: true + + /@colors/colors@1.6.0: + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + dev: false + + /@commitlint/config-validator@18.6.1: + resolution: {integrity: sha512-05uiToBVfPhepcQWE1ZQBR/Io3+tb3gEotZjnI4tTzzPk16NffN6YABgwFQCLmzZefbDcmwWqJWc2XT47q7Znw==} + engines: {node: '>=v18'} + requiresBuild: true + dependencies: + '@commitlint/types': 18.6.1 + ajv: 8.12.0 + dev: true + optional: true + + /@commitlint/execute-rule@18.6.1: + resolution: {integrity: sha512-7s37a+iWyJiGUeMFF6qBlyZciUkF8odSAnHijbD36YDctLhGKoYltdvuJ/AFfRm6cBLRtRk9cCVPdsEFtt/2rg==} + engines: {node: '>=v18'} + requiresBuild: true + dev: true + optional: true + + /@commitlint/load@18.6.1(@types/node@20.11.18)(typescript@5.3.3): + resolution: {integrity: sha512-p26x8734tSXUHoAw0ERIiHyW4RaI4Bj99D8YgUlVV9SedLf8hlWAfyIFhHRIhfPngLlCe0QYOdRKYFt8gy56TA==} + engines: {node: '>=v18'} + requiresBuild: true + dependencies: + '@commitlint/config-validator': 18.6.1 + '@commitlint/execute-rule': 18.6.1 + '@commitlint/resolve-extends': 18.6.1 + '@commitlint/types': 18.6.1 + chalk: 4.1.2 + cosmiconfig: 8.3.6(typescript@5.3.3) + cosmiconfig-typescript-loader: 5.0.0(@types/node@20.11.18)(cosmiconfig@8.3.6)(typescript@5.3.3) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + resolve-from: 5.0.0 + transitivePeerDependencies: + - '@types/node' + - typescript + dev: true + optional: true + + /@commitlint/resolve-extends@18.6.1: + resolution: {integrity: sha512-ifRAQtHwK+Gj3Bxj/5chhc4L2LIc3s30lpsyW67yyjsETR6ctHAHRu1FSpt0KqahK5xESqoJ92v6XxoDRtjwEQ==} + engines: {node: '>=v18'} + requiresBuild: true + dependencies: + '@commitlint/config-validator': 18.6.1 + '@commitlint/types': 18.6.1 + import-fresh: 3.3.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + resolve-global: 1.0.0 + dev: true + optional: true + + /@commitlint/types@18.6.1: + resolution: {integrity: sha512-gwRLBLra/Dozj2OywopeuHj2ac26gjGkz2cZ+86cTJOdtWfiRRr4+e77ZDAGc6MDWxaWheI+mAV5TLWWRwqrFg==} + engines: {node: '>=v18'} + requiresBuild: true + dependencies: + chalk: 4.1.2 + dev: true + optional: true + + /@compodoc/compodoc@1.1.23(typescript@5.3.3): + resolution: {integrity: sha512-5Zfx+CHKTxLD+TxCGt1U8krnEBCWPVxCLt3jCJEN55AzhTluo8xlMenaXlJsuVqL4Lmo/OTTzEXrm9zoQKh/3w==} + engines: {node: '>= 14.0.0'} + hasBin: true + requiresBuild: true + dependencies: + '@angular-devkit/schematics': 14.2.12(chokidar@3.6.0) + '@babel/core': 7.23.9 + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.23.9) + '@babel/preset-env': 7.23.9(@babel/core@7.23.9) + '@compodoc/live-server': 1.2.3 + '@compodoc/ngd-transformer': 2.1.3 + bootstrap.native: 5.0.11 + chalk: 4.1.2 + cheerio: 1.0.0-rc.12 + chokidar: 3.6.0 + colors: 1.4.0 + commander: 11.1.0 + cosmiconfig: 8.3.6(typescript@5.3.3) + decache: 4.6.2 + es6-shim: 0.35.8 + fancy-log: 2.0.0 + fast-glob: 3.3.2 + fs-extra: 11.2.0 + glob: 10.3.10 + handlebars: 4.7.8 + html-entities: 2.4.0 + i18next: 23.8.2 + json5: 2.2.3 + lodash: 4.17.21 + loglevel: 1.9.1 + loglevel-plugin-prefix: 0.8.4 + lunr: 2.3.9 + marked: 7.0.3 + minimist: 1.2.8 + opencollective-postinstall: 2.0.3 + os-name: 4.0.1 + pdfjs-dist: 2.12.313 + pdfmake: 0.2.9 + prismjs: 1.29.0 + semver: 7.6.0 + svg-pan-zoom: 3.6.1 + tablesort: 5.3.0 + traverse: 0.6.8 + ts-morph: 20.0.0 + uuid: 9.0.1 + vis: 4.21.0-EOL + zepto: 1.2.0 + transitivePeerDependencies: + - supports-color + - typescript + - worker-loader + dev: true + + /@compodoc/live-server@1.2.3: + resolution: {integrity: sha512-hDmntVCyjjaxuJzPzBx68orNZ7TW4BtHWMnXlIVn5dqhK7vuFF/11hspO1cMmc+2QTYgqde1TBcb3127S7Zrow==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + chokidar: 3.6.0 + colors: 1.4.0 + connect: 3.7.0 + cors: 2.8.5 + event-stream: 4.0.1 + faye-websocket: 0.11.4 + http-auth: 4.1.9 + http-auth-connect: 1.0.6 + morgan: 1.10.0 + object-assign: 4.1.1 + open: 8.4.0 + proxy-middleware: 0.15.0 + send: 1.0.0-beta.2 + serve-index: 1.9.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@compodoc/ngd-core@2.1.1: + resolution: {integrity: sha512-Z+wE6wWZYVnudRYg6qunDlyh3Orw39Ib66Gvrz5kX5u7So+iu3tr6sQJdqH6yGS3hAjig5avlfhWLlgsb6/x1Q==} + engines: {node: '>= 10.0.0'} + dependencies: + ansi-colors: 4.1.3 + fancy-log: 2.0.0 + typescript: 5.3.3 + dev: true + + /@compodoc/ngd-transformer@2.1.3: + resolution: {integrity: sha512-oWxJza7CpWR8/FeWYfE6j+jgncnGBsTWnZLt5rD2GUpsGSQTuGrsFPnmbbaVLgRS5QIVWBJYke7QFBr/7qVMWg==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aduh95/viz.js': 3.4.0 + '@compodoc/ngd-core': 2.1.1 + dot: 2.0.0-beta.1 + fs-extra: 11.2.0 + dev: true + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + /@dabh/diagnostics@2.0.3: + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + dev: false + + /@emnapi/core@0.45.0: + resolution: {integrity: sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==} + requiresBuild: true + dependencies: + tslib: 2.6.2 + dev: false + optional: true + + /@emnapi/runtime@0.45.0: + resolution: {integrity: sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==} + requiresBuild: true + dependencies: + tslib: 2.6.2 + dev: false + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.57.0: + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@fast-csv/format@4.3.5: + resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==} + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.isboolean: 3.0.3 + lodash.isequal: 4.5.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + dev: false + + /@fast-csv/parse@4.3.6: + resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==} + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.groupby: 4.6.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + lodash.isundefined: 3.0.1 + lodash.uniq: 4.5.0 + dev: false + + /@fastify/accept-negotiator@1.1.0: + resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} + engines: {node: '>=14'} + dev: false + + /@fastify/ajv-compiler@3.5.0: + resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==} + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + fast-uri: 2.3.0 + dev: false + + /@fastify/busboy@1.2.1: + resolution: {integrity: sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==} + engines: {node: '>=14'} + dependencies: + text-decoding: 1.0.0 + dev: false + + /@fastify/cookie@9.3.1: + resolution: {integrity: sha512-h1NAEhB266+ZbZ0e9qUE6NnNR07i7DnNXWG9VbbZ8uC6O/hxHpl+Zoe5sw1yfdZ2U6XhToUGDnzQtWJdCaPwfg==} + dependencies: + cookie-signature: 1.2.1 + fastify-plugin: 4.5.1 + dev: false + + /@fastify/cors@9.0.1: + resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==} + dependencies: + fastify-plugin: 4.5.1 + mnemonist: 0.39.6 + dev: false + + /@fastify/deepmerge@1.3.0: + resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} + dev: false + + /@fastify/error@3.4.1: + resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} + dev: false + + /@fastify/fast-json-stringify-compiler@4.3.0: + resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} + dependencies: + fast-json-stringify: 5.12.0 + dev: false + + /@fastify/formbody@7.4.0: + resolution: {integrity: sha512-H3C6h1GN56/SMrZS8N2vCT2cZr7mIHzBHzOBa5OPpjfB/D6FzP9mMpE02ZzrFX0ANeh0BAJdoXKOF2e7IbV+Og==} + dependencies: + fast-querystring: 1.1.2 + fastify-plugin: 4.5.1 + dev: false + + /@fastify/merge-json-schemas@0.1.1: + resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} + dependencies: + fast-deep-equal: 3.1.3 + dev: false + + /@fastify/middie@8.3.0: + resolution: {integrity: sha512-h+zBxCzMlkEkh4fM7pZaSGzqS7P9M0Z6rXnWPdUEPfe7x1BCj++wEk/pQ5jpyYY4pF8AknFqb77n7uwh8HdxEA==} + dependencies: + '@fastify/error': 3.4.1 + fastify-plugin: 4.5.1 + path-to-regexp: 6.2.1 + reusify: 1.0.4 + dev: false + + /@fastify/multipart@8.1.0: + resolution: {integrity: sha512-sRX9X4ZhAqRbe2kDvXY2NK7i6Wf1Rm2g/CjpGYYM7+Np8E6uWQXcj761j08qPfPO8PJXM+vJ7yrKbK1GPB+OeQ==} + dependencies: + '@fastify/busboy': 1.2.1 + '@fastify/deepmerge': 1.3.0 + '@fastify/error': 3.4.1 + fastify-plugin: 4.5.1 + secure-json-parse: 2.7.0 + stream-wormhole: 1.1.0 + dev: false + + /@fastify/send@2.1.0: + resolution: {integrity: sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==} + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 + fast-decode-uri-component: 1.0.1 + http-errors: 2.0.0 + mime: 3.0.0 + dev: false + + /@fastify/static@7.0.1: + resolution: {integrity: sha512-i1p/nELMknAisNfnjo7yhfoUOdKzA+n92QaMirv2NkZrJ1Wl12v2nyTYlDwPN8XoStMBAnRK/Kx6zKmfrXUPXw==} + dependencies: + '@fastify/accept-negotiator': 1.1.0 + '@fastify/send': 2.1.0 + content-disposition: 0.5.4 + fastify-plugin: 4.5.1 + fastq: 1.17.1 + glob: 10.3.10 + dev: false + + /@foliojs-fork/fontkit@1.9.1: + resolution: {integrity: sha512-U589voc2/ROnvx1CyH9aNzOQWJp127JGU1QAylXGQ7LoEAF6hMmahZLQ4eqAcgHUw+uyW4PjtCItq9qudPkK3A==} + dependencies: + '@foliojs-fork/restructure': 2.0.2 + brfs: 2.0.2 + brotli: 1.3.3 + browserify-optional: 1.0.1 + clone: 1.0.4 + deep-equal: 1.1.2 + dfa: 1.2.0 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + dev: true + + /@foliojs-fork/linebreak@1.1.1: + resolution: {integrity: sha512-pgY/+53GqGQI+mvDiyprvPWgkTlVBS8cxqee03ejm6gKAQNsR1tCYCIvN9FHy7otZajzMqCgPOgC4cHdt4JPig==} + dependencies: + base64-js: 1.3.1 + brfs: 2.0.2 + unicode-trie: 2.0.0 + dev: true + + /@foliojs-fork/pdfkit@0.14.0: + resolution: {integrity: sha512-nMOiQAv6id89MT3tVTCgc7HxD5ZMANwio2o5yvs5sexQkC0KI3BLaLakpsrHmFfeGFAhqPmZATZGbJGXTUebpg==} + dependencies: + '@foliojs-fork/fontkit': 1.9.1 + '@foliojs-fork/linebreak': 1.1.1 + crypto-js: 4.2.0 + png-js: 1.0.0 + dev: true + + /@foliojs-fork/restructure@2.0.2: + resolution: {integrity: sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==} + dev: true + + /@humanwhocodes/config-array@0.11.14: + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.2 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@2.0.2: + resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + dev: true + + /@hutson/parse-repository-url@3.0.2: + resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} + engines: {node: '>=6.9.0'} + dev: true + + /@ioredis/commands@1.2.0: + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + dev: false + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + dev: true + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.11.18 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + dev: true + + /@jest/core@29.7.0(ts-node@10.9.2): + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.18 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.11.18)(ts-node@10.9.2) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.18 + jest-mock: 29.7.0 + dev: true + + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + dev: true + + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 20.11.18 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.22 + '@types/node': 20.11.18 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.1 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.6 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.22 + callsites: 3.1.0 + graceful-fs: 4.2.11 + dev: true + + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + dev: true + + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + dev: true + + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.23.9 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.22 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.5 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 20.11.18 + '@types/yargs': 17.0.32 + chalk: 4.1.2 + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.22 + dev: true + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/source-map@0.3.5: + resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.22 + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + /@jridgewell/trace-mapping@0.3.22: + resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + + /@liaoliaots/nestjs-redis@9.0.5(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(ioredis@5.3.2): + resolution: {integrity: sha512-nPcGLj0zW4mEsYtQYfWx3o7PmrMjuzFk6+t/g2IRopAeWWUZZ/5nIJ4KTKiz/3DJEUkbX8PZqB+dOhklGF0SVA==} + engines: {node: '>=12.22.0'} + peerDependencies: + '@nestjs/common': ^9.0.0 + '@nestjs/core': ^9.0.0 + ioredis: ^5.0.0 + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + ioredis: 5.3.2 + tslib: 2.4.1 + dev: false + + /@ljharb/through@2.3.12: + resolution: {integrity: sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + dev: true + + /@lukeed/csprng@1.1.0: + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + /@lukeed/ms@2.0.2: + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + dev: false + + /@mapbox/node-pre-gyp@1.0.11: + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + requiresBuild: true + dependencies: + detect-libc: 2.0.2 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.6.0 + tar: 6.2.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + optional: true + + /@microsoft/tsdoc@0.14.2: + resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} + dev: false + + /@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2: + resolution: {integrity: sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.2: + resolution: {integrity: sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.2: + resolution: {integrity: sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@msgpackr-extract/msgpackr-extract-linux-arm@3.0.2: + resolution: {integrity: sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@msgpackr-extract/msgpackr-extract-linux-x64@3.0.2: + resolution: {integrity: sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2: + resolution: {integrity: sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@napi-rs/wasm-runtime@0.1.1: + resolution: {integrity: sha512-ATj9ua659JgrkICjJscaeZdmPr44cb/KFjNWuD0N6pux0SpzaM7+iOuuK11mAnQM2N9q0DT4REu6NkL8ZEhopw==} + requiresBuild: true + dependencies: + '@emnapi/core': 0.45.0 + '@emnapi/runtime': 0.45.0 + '@tybys/wasm-util': 0.8.1 + dev: false + optional: true + + /@nestjs-modules/mailer@1.10.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(nodemailer@6.9.9): + resolution: {integrity: sha512-k2gs2NH8Ygq4JnETX+EDBXixLAS8DDZEI/Wbr9LGL3HwO3Qz8zVh8dBJ4ESpySuWniW+a8rARzGXtTUHC4KFlw==} + peerDependencies: + '@nestjs/common': '>=7.0.9' + '@nestjs/core': '>=7.0.9' + nodemailer: '>=6.4.6' + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + css-inline: 0.11.2 + glob: 10.3.10 + mjml: 4.14.1 + nodemailer: 6.9.9 + preview-email: 3.0.19 + optionalDependencies: + '@types/ejs': 3.1.5 + '@types/pug': 2.0.10 + ejs: 3.1.9 + handlebars: 4.7.8 + pug: 3.0.2 + transitivePeerDependencies: + - encoding + dev: false + + /@nestjs/axios@3.0.2(@nestjs/common@10.3.3)(axios@1.6.7)(rxjs@7.8.1): + resolution: {integrity: sha512-Z6GuOUdNQjP7FX+OuV2Ybyamse+/e0BFdTWBX5JxpBDKA+YkdLynDgG6HTF04zy6e9zPa19UX0WA2VDoehwhXQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + axios: ^1.3.1 + rxjs: ^6.0.0 || ^7.0.0 + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + axios: 1.6.7 + rxjs: 7.8.1 + dev: false + + /@nestjs/bull-shared@10.1.0(@nestjs/common@10.3.3)(@nestjs/core@10.3.3): + resolution: {integrity: sha512-E1lAvVTCwbtBXySElkVrleXzr1bNuTCOLaQ1GmLSQGGlzXIvrXFXEIS1Dh1JCULICC25b7rGOfD3yL7uKRaMzw==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + tslib: 2.6.2 + dev: false + + /@nestjs/bull@10.1.0(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(bull@4.12.2): + resolution: {integrity: sha512-JEw4eFCtgECg1A9UGxa8eJtaxjwSk2XPLAG1xahZGnoozAYlDzvO6W6mFpCbKvoBbNSh1p+p+lccUbrbQnUd8w==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + bull: ^3.3 || ^4.0.0 + dependencies: + '@nestjs/bull-shared': 10.1.0(@nestjs/common@10.3.3)(@nestjs/core@10.3.3) + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + bull: 4.12.2 + tslib: 2.6.2 + dev: false + + /@nestjs/cache-manager@2.2.1(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(cache-manager@5.4.0)(rxjs@7.8.1): + resolution: {integrity: sha512-mXj0zenuyMPJICokwVud4Kjh0+pzBNBAgfpx3I48LozNkd8Qfv/MAhZsb15GihGpbFRxafUo3p6XvtAqRm8GRw==} + peerDependencies: + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + cache-manager: <=5 + rxjs: ^7.0.0 + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + cache-manager: 5.4.0 + rxjs: 7.8.1 + dev: false + + /@nestjs/cli@10.3.2: + resolution: {integrity: sha512-aWmD1GLluWrbuC4a1Iz/XBk5p74Uj6nIVZj6Ov03JbTfgtWqGFLtXuMetvzMiHxfrHehx/myt2iKAPRhKdZvTg==} + engines: {node: '>= 16.14'} + hasBin: true + peerDependencies: + '@swc/cli': ^0.1.62 || ^0.3.0 + '@swc/core': ^1.3.62 + peerDependenciesMeta: + '@swc/cli': + optional: true + '@swc/core': + optional: true + dependencies: + '@angular-devkit/core': 17.1.2(chokidar@3.6.0) + '@angular-devkit/schematics': 17.1.2(chokidar@3.6.0) + '@angular-devkit/schematics-cli': 17.1.2(chokidar@3.6.0) + '@nestjs/schematics': 10.1.1(chokidar@3.6.0)(typescript@5.3.3) + chalk: 4.1.2 + chokidar: 3.6.0 + cli-table3: 0.6.3 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.3.3)(webpack@5.90.1) + glob: 10.3.10 + inquirer: 8.2.6 + node-emoji: 1.11.0 + ora: 5.4.1 + rimraf: 4.4.1 + shelljs: 0.8.5 + source-map-support: 0.5.21 + tree-kill: 1.2.2 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.1.0 + typescript: 5.3.3 + webpack: 5.90.1 + webpack-node-externals: 3.0.0 + transitivePeerDependencies: + - esbuild + - uglify-js + - webpack-cli + dev: true + + /@nestjs/common@10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1): + resolution: {integrity: sha512-LAkTe8/CF0uNWM0ecuDwUNTHCi1lVSITmmR4FQ6Ftz1E7ujQCnJ5pMRzd8JRN14vdBkxZZ8VbVF0BDUKoKNxMQ==} + peerDependencies: + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 + iterare: 1.2.1 + reflect-metadata: 0.2.1 + rxjs: 7.8.1 + tslib: 2.6.2 + uid: 2.0.2 + + /@nestjs/config@3.2.0(@nestjs/common@10.3.3)(rxjs@7.8.1): + resolution: {integrity: sha512-BpYRn57shg7CH35KGT6h+hT7ZucB6Qn2B3NBNdvhD4ApU8huS5pX/Wc2e/aO5trIha606Bz2a9t9/vbiuTBTww==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + rxjs: ^7.1.0 + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + dotenv: 16.4.1 + dotenv-expand: 10.0.0 + lodash: 4.17.21 + rxjs: 7.8.1 + uuid: 9.0.1 + dev: false + + /@nestjs/core@10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1): + resolution: {integrity: sha512-kxJWggQAPX3RuZx9JVec69eSLaYLNIox2emkZJpfBJ5Qq7cAq7edQIt1r4LGjTKq6kFubNTPsqhWf5y7yFRBPw==} + requiresBuild: true + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/microservices': ^10.0.0 + '@nestjs/platform-express': ^10.0.0 + '@nestjs/websockets': ^10.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/websockets': 10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(@nestjs/platform-socket.io@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nuxtjs/opencollective': 0.3.2 + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 3.2.0 + reflect-metadata: 0.2.1 + rxjs: 7.8.1 + tslib: 2.6.2 + uid: 2.0.2 + transitivePeerDependencies: + - encoding + + /@nestjs/event-emitter@2.0.4(@nestjs/common@10.3.3)(@nestjs/core@10.3.3): + resolution: {integrity: sha512-quMiw8yOwoSul0pp3mOonGz8EyXWHSBTqBy8B0TbYYgpnG1Ix2wGUnuTksLWaaBiiOTDhciaZ41Y5fJZsSJE1Q==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + eventemitter2: 6.4.9 + dev: false + + /@nestjs/jwt@10.2.0(@nestjs/common@10.3.3): + resolution: {integrity: sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@types/jsonwebtoken': 9.0.5 + jsonwebtoken: 9.0.2 + dev: false + + /@nestjs/mapped-types@2.0.5(@nestjs/common@10.3.3)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1): + resolution: {integrity: sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + class-transformer: 0.5.1 + class-validator: 0.14.1 + reflect-metadata: 0.2.1 + dev: false + + /@nestjs/passport@10.0.3(@nestjs/common@10.3.3)(passport@0.7.0): + resolution: {integrity: sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + passport: ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + passport: 0.7.0 + dev: false + + /@nestjs/platform-fastify@10.3.3(@fastify/static@7.0.1)(@nestjs/common@10.3.3)(@nestjs/core@10.3.3): + resolution: {integrity: sha512-OTKcKGnWWrSk/nDl5bFmv2gcPhbF6nsU/EHxkh6tguc0YY4aopQR9GaodseJn8isEOtZzcx8UUBsnLTtqWKxaA==} + peerDependencies: + '@fastify/static': ^6.0.0 || ^7.0.0 + '@fastify/view': ^7.0.0 || ^8.0.0 + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + '@fastify/view': + optional: true + dependencies: + '@fastify/cors': 9.0.1 + '@fastify/formbody': 7.4.0 + '@fastify/middie': 8.3.0 + '@fastify/static': 7.0.1 + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + fastify: 4.26.0 + light-my-request: 5.11.0 + path-to-regexp: 3.2.0 + tslib: 2.6.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@nestjs/platform-socket.io@10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(rxjs@7.8.1): + resolution: {integrity: sha512-QqM9BMTdYPvXOqx3oWrv130HOtc2krPvfgqgDsPWkBLfR+TssrA5QDaTW8HSjEQAfmugvHwhEAAU4+yXRl6tKg==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/websockets': ^10.0.0 + rxjs: ^7.1.0 + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/websockets': 10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(@nestjs/platform-socket.io@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + rxjs: 7.8.1 + socket.io: 4.7.4 + tslib: 2.6.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + /@nestjs/schedule@4.0.1(@nestjs/common@10.3.3)(@nestjs/core@10.3.3): + resolution: {integrity: sha512-cz2FNjsuoma+aGsG0cMmG6Dqg/BezbBWet1UTHtAuu6d2mXNTVcmoEQM2DIVG5Lfwb2hfSE2yZt8Moww+7y+mA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + cron: 3.1.6 + uuid: 9.0.1 + dev: false + + /@nestjs/schematics@10.1.1(chokidar@3.6.0)(typescript@5.3.3): + resolution: {integrity: sha512-o4lfCnEeIkfJhGBbLZxTuVWcGuqDCFwg5OrvpgRUBM7vI/vONvKKiB5riVNpO+JqXoH0I42NNeDb0m4V5RREig==} + peerDependencies: + typescript: '>=4.8.2' + dependencies: + '@angular-devkit/core': 17.1.2(chokidar@3.6.0) + '@angular-devkit/schematics': 17.1.2(chokidar@3.6.0) + comment-json: 4.2.3 + jsonc-parser: 3.2.1 + pluralize: 8.0.0 + typescript: 5.3.3 + transitivePeerDependencies: + - chokidar + dev: true + + /@nestjs/swagger@7.3.0(@fastify/static@7.0.1)(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1): + resolution: {integrity: sha512-zLkfKZ+ioYsIZ3dfv7Bj8YHnZMNAGWFUmx2ZDuLp/fBE4P8BSjB7hldzDueFXsmwaPL90v7lgyd82P+s7KME1Q==} + peerDependencies: + '@fastify/static': ^6.0.0 || ^7.0.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + '@fastify/static': 7.0.1 + '@microsoft/tsdoc': 0.14.2 + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/mapped-types': 2.0.5(@nestjs/common@10.3.3)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1) + class-transformer: 0.5.1 + class-validator: 0.14.1 + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 3.2.0 + reflect-metadata: 0.2.1 + swagger-ui-dist: 5.11.2 + dev: false + + /@nestjs/terminus@10.2.2(@nestjs/axios@3.0.2)(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(@nestjs/typeorm@10.0.2)(reflect-metadata@0.2.1)(rxjs@7.8.1)(typeorm@0.3.17): + resolution: {integrity: sha512-tZdTSqgHyxekN8PJmJJ1ptZG97q/1nBIBwLdMcmB7Dsz4XDTQvYuhs20F1qkEgFuQwarNkb/2AF5Qib31g2bmA==} + peerDependencies: + '@grpc/grpc-js': '*' + '@grpc/proto-loader': '*' + '@mikro-orm/core': '*' + '@mikro-orm/nestjs': '*' + '@nestjs/axios': ^1.0.0 || ^2.0.0 || ^3.0.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + '@nestjs/microservices': ^9.0.0 || ^10.0.0 + '@nestjs/mongoose': ^9.0.0 || ^10.0.0 + '@nestjs/sequelize': ^9.0.0 || ^10.0.0 + '@nestjs/typeorm': ^9.0.0 || ^10.0.0 + '@prisma/client': '*' + mongoose: '*' + reflect-metadata: 0.1.x || 0.2.x + rxjs: 7.x + sequelize: '*' + typeorm: '*' + peerDependenciesMeta: + '@grpc/grpc-js': + optional: true + '@grpc/proto-loader': + optional: true + '@mikro-orm/core': + optional: true + '@mikro-orm/nestjs': + optional: true + '@nestjs/axios': + optional: true + '@nestjs/microservices': + optional: true + '@nestjs/mongoose': + optional: true + '@nestjs/sequelize': + optional: true + '@nestjs/typeorm': + optional: true + '@prisma/client': + optional: true + mongoose: + optional: true + sequelize: + optional: true + typeorm: + optional: true + dependencies: + '@nestjs/axios': 3.0.2(@nestjs/common@10.3.3)(axios@1.6.7)(rxjs@7.8.1) + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/typeorm': 10.0.2(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1)(typeorm@0.3.17) + boxen: 5.1.2 + check-disk-space: 3.4.0 + reflect-metadata: 0.2.1 + rxjs: 7.8.1 + typeorm: 0.3.17(ioredis@5.3.2)(mysql2@3.9.1)(ts-node@10.9.2) + dev: false + + /@nestjs/testing@10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3): + resolution: {integrity: sha512-kX20GfjAImL5grd/i69uD/x7sc00BaqGcP2dRG3ilqshQUuy5DOmspLCr3a2C8xmVU7kzK4spT0oTxhe6WcCAA==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + '@nestjs/microservices': ^10.0.0 + '@nestjs/platform-express': ^10.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + tslib: 2.6.2 + dev: true + + /@nestjs/throttler@5.1.2(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(reflect-metadata@0.2.1): + resolution: {integrity: sha512-60MqhSLYUqWOgc38P6C6f76JIpf6mVjly7gpuPBCKtVd0p5e8Fq855j7bJuO4/v25vgaOo1OdVs0U1qtgYioGw==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + reflect-metadata: 0.2.1 + dev: false + + /@nestjs/typeorm@10.0.2(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1)(typeorm@0.3.17): + resolution: {integrity: sha512-H738bJyydK4SQkRCTeh1aFBxoO1E9xdL/HaLGThwrqN95os5mEyAtK7BLADOS+vldP4jDZ2VQPLj4epWwRqCeQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + rxjs: ^7.2.0 + typeorm: ^0.3.0 + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + reflect-metadata: 0.2.1 + rxjs: 7.8.1 + typeorm: 0.3.17(ioredis@5.3.2)(mysql2@3.9.1)(ts-node@10.9.2) + uuid: 9.0.1 + dev: false + + /@nestjs/websockets@10.3.3(@nestjs/common@10.3.3)(@nestjs/core@10.3.3)(@nestjs/platform-socket.io@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1): + resolution: {integrity: sha512-cR5cB0bLS87vd0iu7Nud/4x2EH1Vs0aIgwGWd0eH/5SAw0rrDNU81PiOde+rnMXETbxvSVfOZuLRyn7/WQtGUg==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + '@nestjs/platform-socket.io': ^10.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/platform-socket.io': + optional: true + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/platform-socket.io': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(rxjs@7.8.1) + iterare: 1.2.1 + object-hash: 3.0.0 + reflect-metadata: 0.2.1 + rxjs: 7.8.1 + tslib: 2.6.2 + + /@node-rs/jieba-android-arm-eabi@1.10.0: + resolution: {integrity: sha512-bzusJSLHm7I0qL8aQXGLt7IQ51Px35yGGEcQ/Ps4SEt0AxRSJ2/rxNET/8mlwBpOCZ5xiKE3BOBRfQajiPiI3g==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@node-rs/jieba-android-arm64@1.10.0: + resolution: {integrity: sha512-g89Oq5U2RPmtlvuQhjNj8YZc5Gq033ODb7Ot4Z/OdIHvg2WMxi2M1GQhcdKu60dO79/tazc53W6I8/y691DUfQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@node-rs/jieba-darwin-arm64@1.10.0: + resolution: {integrity: sha512-IhR5r+XxFcfhVsF93zQ3uCJy8ndotRntXzoW/JCyKqOahUo/ITQRT6vTKHKMyD9xNmjl222OZonBSo2+mlI2fQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@node-rs/jieba-darwin-x64@1.10.0: + resolution: {integrity: sha512-MBIs8ixKY4FPnifdZ7eTx6ht85TXE4kFBK4c8A/VDAbnmzBzpEyuV7tHUA2wAdfR0muC9j7/5FB4kQGZgYfc8g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@node-rs/jieba-freebsd-x64@1.10.0: + resolution: {integrity: sha512-MuY+1QEXONxo3I/uFLFju0/pSN5bzQORhJkIdP8CYv+jZaVB4Uz6rC7A5HrgjiAXOna6QsKlRgx2bYyHfaBUrA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /@node-rs/jieba-linux-arm-gnueabihf@1.10.0: + resolution: {integrity: sha512-QfSBnwISdVuTqsi4iThAO1LSbKRSqSsIWiIJgCduhYsTDDiG9+pHyfiZtcTwSf73SDXHZ400QuBNONWLQ/dSag==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@node-rs/jieba-linux-arm64-gnu@1.10.0: + resolution: {integrity: sha512-vzA2tX/6dReEd/7tZ9927glWQmKDausM6R9S5CqZx4BA4NSaWAK0xFdWsz0K7np459FXqNavLdNB5FVFJb4zzA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + requiresBuild: true + dev: false + optional: true + + /@node-rs/jieba-linux-arm64-musl@1.10.0: + resolution: {integrity: sha512-gxqoAVOQsn9sgYK6mFO9dsMZ/yOMvVecLZW5rGvLErjiugVvYUlESXIvCqxp2GSws8RtTqJj6p9u/lBmCCuvaw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + requiresBuild: true + dev: false + optional: true + + /@node-rs/jieba-linux-x64-gnu@1.10.0: + resolution: {integrity: sha512-rS5Shs8JITxJjFIjoIZ5a9O+GO21TJgKu03g2qwFE3QaN5ZOvXtz+/AqqyfT4GmmMhCujD83AGqfOGXDmItF9w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + requiresBuild: true + dev: false + optional: true + + /@node-rs/jieba-linux-x64-musl@1.10.0: + resolution: {integrity: sha512-BvSiF2rR8Birh2oEVHcYwq0WGC1cegkEdddWsPrrSmpKmukJE2zyjcxaOOggq2apb8fIRsjyeeUh6X3R5AgjvA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + requiresBuild: true + dev: false + optional: true + + /@node-rs/jieba-wasm32-wasi@1.10.0: + resolution: {integrity: sha512-EzeAAbRrFTdYw61rd8Mfwdp/fA21d58z9vLY06CDbI+dqANfMFn1IUdwzKWi8S5J/MRhvbzonbbh3yHlz6F43Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + requiresBuild: true + dependencies: + '@napi-rs/wasm-runtime': 0.1.1 + dev: false + optional: true + + /@node-rs/jieba-win32-arm64-msvc@1.10.0: + resolution: {integrity: sha512-eZjRLFUAvq1/E5+xXfJRqIB99Gu6BA+6+EXf/rCLuvEjXrDQuUunhmrSoOL5MjmUXTtazS+bXq9PXV5EFYyOPw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@node-rs/jieba-win32-ia32-msvc@1.10.0: + resolution: {integrity: sha512-DrfbeCN7UcLN+MiocZabWo74XZIjfpQsJ/WMOItZzVbU2gDcJSkSyAhML9+OqId66DhGCMFFlGinocElM8iIAw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@node-rs/jieba-win32-x64-msvc@1.10.0: + resolution: {integrity: sha512-RjBkBmjjHmj+bofiq5/han8wzbCkDk24OAPJ+YX8PX20GFSHmdjCiWapv3AooN8/RiKqlBfgodjS1JUngNWo5g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@node-rs/jieba@1.10.0: + resolution: {integrity: sha512-9oZMCvZVnrAMeWTSnEjJ0OSw7YcV4dJJKSioqq80oUNf3eYLGdEXsgYwCe1AYEMcfUfNVgvjznItJKrsoud0IA==} + engines: {node: '>= 10'} + requiresBuild: true + optionalDependencies: + '@node-rs/jieba-android-arm-eabi': 1.10.0 + '@node-rs/jieba-android-arm64': 1.10.0 + '@node-rs/jieba-darwin-arm64': 1.10.0 + '@node-rs/jieba-darwin-x64': 1.10.0 + '@node-rs/jieba-freebsd-x64': 1.10.0 + '@node-rs/jieba-linux-arm-gnueabihf': 1.10.0 + '@node-rs/jieba-linux-arm64-gnu': 1.10.0 + '@node-rs/jieba-linux-arm64-musl': 1.10.0 + '@node-rs/jieba-linux-x64-gnu': 1.10.0 + '@node-rs/jieba-linux-x64-musl': 1.10.0 + '@node-rs/jieba-wasm32-wasi': 1.10.0 + '@node-rs/jieba-win32-arm64-msvc': 1.10.0 + '@node-rs/jieba-win32-ia32-msvc': 1.10.0 + '@node-rs/jieba-win32-x64-msvc': 1.10.0 + dev: false + optional: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + dev: true + + /@nuxtjs/opencollective@0.3.2: + resolution: {integrity: sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + dependencies: + chalk: 4.1.2 + consola: 2.15.3 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + /@one-ini/wasm@0.1.1: + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + dev: false + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + optional: true + + /@selderee/plugin-htmlparser2@0.11.0: + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + dev: false + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + + /@sinonjs/commons@3.0.1: + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.1 + dev: true + + /@socket.io/component-emitter@3.1.0: + resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + + /@socket.io/redis-adapter@8.2.1(socket.io-adapter@2.5.2): + resolution: {integrity: sha512-6Dt7EZgGSBP0qvXeOKGx7NnSr2tPMbVDfDyL97zerZo+v69hMfL99skMCL3RKZlWVqLyRme2T0wcy3udHhtOsg==} + engines: {node: '>=10.0.0'} + peerDependencies: + socket.io-adapter: ^2.4.0 + dependencies: + debug: 4.3.4 + notepack.io: 3.0.1 + socket.io-adapter: 2.5.2 + uid2: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@socket.io/redis-emitter@5.1.0: + resolution: {integrity: sha512-QQUFPBq6JX7JIuM/X1811ymKlAfwufnQ8w6G2/59Jaqp09hdF1GJ/+e8eo/XdcmT0TqkvcSa2TT98ggTXa5QYw==} + dependencies: + debug: 4.3.4 + notepack.io: 3.0.1 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - supports-color + dev: false + + /@sqltools/formatter@1.2.5: + resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + dev: false + + /@thednp/event-listener@2.0.4: + resolution: {integrity: sha512-sc4B7AzYAIvnGnivirq0XyR7LfzEDhGiiB70Q0qdNn8wSJ2pL1buVAsEZxrlc47qRJiBV4YIP+BFkyMm2r3NLg==} + engines: {node: '>=16', pnpm: '>=8.6.0'} + dev: true + + /@thednp/shorty@2.0.0: + resolution: {integrity: sha512-kwtLivCxYIoFfGIVU4NlZtfdA/zxZ6X8UcWaJrb7XqU3WQ4Q1p5IaZlLBfOVAO06WH5oWE87QUdK/dS56Wnfjg==} + engines: {node: '>=16', pnpm: '>=8.6.0'} + dev: true + + /@ts-morph/common@0.21.0: + resolution: {integrity: sha512-ES110Mmne5Vi4ypUKrtVQfXFDtCsDXiUiGxF6ILVlE90dDD4fdpC1LSjydl/ml7xJWKSDZwUYD2zkOePMSrPBA==} + dependencies: + fast-glob: 3.3.2 + minimatch: 7.4.6 + mkdirp: 2.1.6 + path-browserify: 1.0.1 + dev: true + + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + /@tybys/wasm-util@0.8.1: + resolution: {integrity: sha512-GSsTwyBl4pIzsxAY5wroZdyQKyhXk0d8PCRZtrSZ2WEB1cBdrp2EgGBwHOGCZtIIPun/DL3+AykCv+J6fyRH4Q==} + requiresBuild: true + dependencies: + tslib: 2.6.2 + dev: false + optional: true + + /@types/babel__core@7.20.5: + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + dependencies: + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.5 + dev: true + + /@types/babel__generator@7.6.8: + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@types/babel__template@7.4.4: + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + dependencies: + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 + dev: true + + /@types/babel__traverse@7.20.5: + resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} + dependencies: + '@babel/types': 7.23.9 + dev: true + + /@types/body-parser@1.19.5: + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.11.18 + dev: true + + /@types/cache-manager@4.0.6: + resolution: {integrity: sha512-8qL93MF05/xrzFm/LSPtzNEOE1eQF3VwGHAcQEylgp5hDSTe41jtFwbSYAPfyYcVa28y1vYSjIt0c1fLLUiC/Q==} + dev: true + + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + dependencies: + '@types/node': 20.11.18 + dev: true + + /@types/cookie@0.4.1: + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + + /@types/cookiejar@2.1.5: + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + dev: true + + /@types/cors@2.8.17: + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + dependencies: + '@types/node': 20.11.18 + + /@types/ejs@3.1.5: + resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==} + dev: false + optional: true + + /@types/eslint-scope@3.7.7: + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + dependencies: + '@types/eslint': 8.56.2 + '@types/estree': 1.0.5 + dev: true + + /@types/eslint@8.56.2: + resolution: {integrity: sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==} + dependencies: + '@types/estree': 1.0.5 + '@types/json-schema': 7.0.15 + dev: true + + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: true + + /@types/express-serve-static-core@4.17.43: + resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} + dependencies: + '@types/node': 20.11.18 + '@types/qs': 6.9.11 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + dev: true + + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.17.43 + '@types/qs': 6.9.11 + '@types/serve-static': 1.15.5 + dev: true + + /@types/graceful-fs@4.1.9: + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + dependencies: + '@types/node': 20.11.18 + dev: true + + /@types/http-errors@2.0.4: + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + dev: true + + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + dev: true + + /@types/istanbul-lib-report@3.0.3: + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + dev: true + + /@types/istanbul-reports@3.0.4: + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + dependencies: + '@types/istanbul-lib-report': 3.0.3 + dev: true + + /@types/jest@29.5.12: + resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + dev: true + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true + + /@types/jsonwebtoken@9.0.5: + resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} + dependencies: + '@types/node': 20.11.18 + dev: false + + /@types/lodash@4.14.202: + resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} + dev: false + + /@types/luxon@3.3.8: + resolution: {integrity: sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ==} + dev: false + + /@types/methods@1.1.4: + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + dev: true + + /@types/mime@1.3.5: + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + dev: true + + /@types/mime@3.0.4: + resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} + dev: true + + /@types/minimist@1.2.5: + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + dev: true + + /@types/multer@1.4.11: + resolution: {integrity: sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==} + dependencies: + '@types/express': 4.17.21 + dev: true + + /@types/node@14.18.63: + resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} + dev: false + + /@types/node@20.11.18: + resolution: {integrity: sha512-ABT5VWnnYneSBcNWYSCuR05M826RoMyMSGiFivXGx6ZUIsXb9vn4643IEwkg2zbEOSgAiSogtapN2fgc4mAPlw==} + dependencies: + undici-types: 5.26.5 + + /@types/normalize-package-data@2.4.4: + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + dev: true + + /@types/parse-json@4.0.2: + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + requiresBuild: true + dev: false + optional: true + + /@types/pug@2.0.10: + resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} + dev: false + optional: true + + /@types/qs@6.9.11: + resolution: {integrity: sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==} + dev: true + + /@types/range-parser@1.2.7: + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + dev: true + + /@types/semver@7.5.7: + resolution: {integrity: sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==} + dev: true + + /@types/send@0.17.4: + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.11.18 + dev: true + + /@types/serve-static@1.15.5: + resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==} + dependencies: + '@types/http-errors': 2.0.4 + '@types/mime': 3.0.4 + '@types/node': 20.11.18 + dev: true + + /@types/stack-utils@2.0.3: + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + dev: true + + /@types/superagent@8.1.3: + resolution: {integrity: sha512-R/CfN6w2XsixLb1Ii8INfn+BT9sGPvw74OavfkW4SwY+jeUcAwLZv2+bXLJkndnimxjEBm0RPHgcjW9pLCa8cw==} + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 20.11.18 + dev: true + + /@types/supertest@6.0.2: + resolution: {integrity: sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==} + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.3 + dev: true + + /@types/triple-beam@1.3.5: + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + dev: false + + /@types/ua-parser-js@0.7.39: + resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==} + dev: true + + /@types/validator@13.11.9: + resolution: {integrity: sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==} + + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + dev: true + + /@types/yargs@17.0.32: + resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + dependencies: + '@types/yargs-parser': 21.0.3 + dev: true + + /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.3.3): + resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare-lite: 1.4.0 + semver: 7.6.0 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.3.3): + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.57.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@5.62.0: + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + dev: true + + /@typescript-eslint/type-utils@5.62.0(eslint@8.57.0)(typescript@5.3.3): + resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.57.0 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@5.62.0: + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.3.3): + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.6.0 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.3.3): + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.7 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) + eslint: 8.57.0 + eslint-scope: 5.1.1 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@5.62.0: + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + + /@webassemblyjs/ast@1.11.6: + resolution: {integrity: sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==} + dependencies: + '@webassemblyjs/helper-numbers': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + dev: true + + /@webassemblyjs/floating-point-hex-parser@1.11.6: + resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} + dev: true + + /@webassemblyjs/helper-api-error@1.11.6: + resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} + dev: true + + /@webassemblyjs/helper-buffer@1.11.6: + resolution: {integrity: sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==} + dev: true + + /@webassemblyjs/helper-numbers@1.11.6: + resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/helper-wasm-bytecode@1.11.6: + resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} + dev: true + + /@webassemblyjs/helper-wasm-section@1.11.6: + resolution: {integrity: sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + dev: true + + /@webassemblyjs/ieee754@1.11.6: + resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} + dependencies: + '@xtuc/ieee754': 1.2.0 + dev: true + + /@webassemblyjs/leb128@1.11.6: + resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} + dependencies: + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/utf8@1.11.6: + resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} + dev: true + + /@webassemblyjs/wasm-edit@1.11.6: + resolution: {integrity: sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/helper-wasm-section': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + '@webassemblyjs/wasm-opt': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + '@webassemblyjs/wast-printer': 1.11.6 + dev: true + + /@webassemblyjs/wasm-gen@1.11.6: + resolution: {integrity: sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + dev: true + + /@webassemblyjs/wasm-opt@1.11.6: + resolution: {integrity: sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-buffer': 1.11.6 + '@webassemblyjs/wasm-gen': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + dev: true + + /@webassemblyjs/wasm-parser@1.11.6: + resolution: {integrity: sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + dev: true + + /@webassemblyjs/wast-printer@1.11.6: + resolution: {integrity: sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==} + dependencies: + '@webassemblyjs/ast': 1.11.6 + '@xtuc/long': 4.2.2 + dev: true + + /@xtuc/ieee754@1.2.0: + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + dev: true + + /@xtuc/long@4.2.2: + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + dev: true + + /@zxing/text-encoding@0.9.0: + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + requiresBuild: true + dev: false + optional: true + + /JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + dev: true + + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + requiresBuild: true + dev: false + optional: true + + /abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: false + + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + + /abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + dev: false + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + /acorn-import-assertions@1.9.0(acorn@8.11.3): + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.11.3 + dev: true + + /acorn-jsx@5.3.2(acorn@8.11.3): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.11.3 + dev: true + + /acorn-node@1.8.2: + resolution: {integrity: sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==} + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + xtend: 4.0.2 + dev: true + + /acorn-walk@7.2.0: + resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + + /acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + + /add-stream@1.0.0: + resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==} + dev: true + + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + requiresBuild: true + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + optional: true + + /agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} + dependencies: + humanize-ms: 1.2.1 + dev: false + + /ajv-formats@2.1.1(ajv@8.11.0): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.11.0 + dev: true + + /ajv-formats@2.1.1(ajv@8.12.0): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.12.0 + + /ajv-keywords@3.5.2(ajv@6.12.6): + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + dependencies: + ajv: 6.12.6 + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ajv@8.11.0: + resolution: {integrity: sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: true + + /ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + /alce@1.2.0: + resolution: {integrity: sha512-XppPf2S42nO2WhvKzlwzlfcApcXHzjlod30pKmcWjRgLOtqoe5DMuqdiYoM6AgyXksc6A6pV4v1L/WW217e57w==} + engines: {node: '>=0.8.0'} + dependencies: + esprima: 1.2.5 + estraverse: 1.9.3 + dev: false + + /amdefine@1.0.1: + resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} + engines: {node: '>=0.4.2'} + requiresBuild: true + dev: true + optional: true + + /ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + dependencies: + string-width: 4.2.3 + dev: false + + /ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + /ansi-escapes@3.2.0: + resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} + engines: {node: '>=4'} + dev: true + + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: true + + /ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + dev: true + + /ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: false + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + /apache-crypt@1.2.6: + resolution: {integrity: sha512-072WetlM4blL8PREJVeY+WHiUh1R5VNt2HfceGS8aKqttPHcmqE5pkKuXPz/ULmJOFkc8Hw3kfKl6vy7Qka6DA==} + engines: {node: '>=8'} + dependencies: + unix-crypt-td-js: 1.1.4 + dev: true + + /apache-md5@1.1.8: + resolution: {integrity: sha512-FCAJojipPn0bXjuEpjOOOMN8FZDkxfWWp4JGN9mifU2IhxvKyXZYqpzPHdnTSUpmPDy+tsslB6Z1g+Vg6nVbYA==} + engines: {node: '>=8'} + dev: true + + /app-root-path@3.1.0: + resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} + engines: {node: '>= 6.0.0'} + dev: false + + /aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + requiresBuild: true + dev: false + optional: true + + /archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + dev: false + + /archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + dev: false + + /archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + dependencies: + archiver-utils: 2.1.0 + async: 3.2.5 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + dev: false + + /archy@1.0.0: + resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} + dev: false + + /are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + requiresBuild: true + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + dev: false + optional: true + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + /array-from@2.1.1: + resolution: {integrity: sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==} + dev: true + + /array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + dev: true + + /array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: true + + /asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + /assert-never@1.2.1: + resolution: {integrity: sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==} + requiresBuild: true + dev: false + + /ast-transform@0.0.0: + resolution: {integrity: sha512-e/JfLiSoakfmL4wmTGPjv0HpTICVmxwXgYOB8x+mzozHL8v+dSfCbrJ8J8hJ0YBP0XcYu1aLZ6b/3TnxNK3P2A==} + dependencies: + escodegen: 1.2.0 + esprima: 1.0.4 + through: 2.3.8 + dev: true + + /ast-types@0.7.8: + resolution: {integrity: sha512-RIOpVnVlltB6PcBJ5BMLx+H+6JJ/zjDGU0t7f0L6c2M1dqcK92VQopLBlPQ9R80AVXelfqYgjcPLtHtDbNFg0Q==} + engines: {node: '>= 0.6'} + dev: true + + /async@3.2.5: + resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + dev: false + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + /at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + dev: true + + /atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + dev: false + + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: false + + /avvio@8.3.0: + resolution: {integrity: sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==} + dependencies: + '@fastify/error': 3.4.1 + archy: 1.0.0 + debug: 4.3.4 + fastq: 1.17.1 + transitivePeerDependencies: + - supports-color + dev: false + + /axios@1.6.7: + resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} + dependencies: + follow-redirects: 1.15.5 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + + /babel-jest@29.7.0(@babel/core@7.23.9): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.23.9 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.23.9) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + dependencies: + '@babel/helper-plugin-utils': 7.22.5 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.23.9 + '@babel/types': 7.23.9 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.5 + dev: true + + /babel-plugin-macros@2.8.0: + resolution: {integrity: sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==} + requiresBuild: true + dependencies: + '@babel/runtime': 7.23.9 + cosmiconfig: 6.0.0 + resolve: 1.22.8 + dev: false + optional: true + + /babel-plugin-polyfill-corejs2@0.4.8(@babel/core@7.23.9): + resolution: {integrity: sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/compat-data': 7.23.5 + '@babel/core': 7.23.9 + '@babel/helper-define-polyfill-provider': 0.5.0(@babel/core@7.23.9) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-corejs3@0.9.0(@babel/core@7.23.9): + resolution: {integrity: sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-define-polyfill-provider': 0.5.0(@babel/core@7.23.9) + core-js-compat: 3.36.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-regenerator@0.5.5(@babel/core@7.23.9): + resolution: {integrity: sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/helper-define-polyfill-provider': 0.5.0(@babel/core@7.23.9) + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-preval@4.0.0: + resolution: {integrity: sha512-fZI/4cYneinlj2k/FsXw0/lTWSC5KKoepUueS1g25Gb5vx3GrRyaVwxWCshYqx11GEU4mZnbbFhee8vpquFS2w==} + engines: {node: '>=8', npm: '>=6'} + requiresBuild: true + dependencies: + '@babel/runtime': 7.23.9 + babel-plugin-macros: 2.8.0 + require-from-string: 2.0.2 + dev: false + optional: true + + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.9): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.9 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.9) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.9) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.9) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.9) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.9) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.9) + dev: true + + /babel-preset-jest@29.6.3(@babel/core@7.23.9): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.9 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.9) + dev: true + + /babel-walk@3.0.0-canary-5: + resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} + engines: {node: '>= 10.0.0'} + requiresBuild: true + dependencies: + '@babel/types': 7.23.9 + dev: false + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /base64-js@1.3.1: + resolution: {integrity: sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==} + dev: true + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + /base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + /base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + dev: false + + /basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + dependencies: + safe-buffer: 5.1.2 + dev: true + + /batch@0.6.1: + resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + dev: true + + /bcryptjs@2.4.3: + resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + dev: true + + /before@0.0.1: + resolution: {integrity: sha512-1J5SWbkoVJH9DTALN8igB4p+nPKZzPrJ/HomqBDLpfUvDXCdjdBmBUcH5McZfur0lftVssVU6BZug5NYh87zTw==} + dev: false + + /big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + dev: false + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + + /binary@0.3.0: + resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} + dependencies: + buffers: 0.1.1 + chainsaw: 0.1.0 + dev: false + + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + /block-stream2@2.1.0: + resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==} + dependencies: + readable-stream: 3.6.2 + dev: false + + /bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + dev: false + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + /bootstrap.native@5.0.11: + resolution: {integrity: sha512-bk2i4sQcQk2KuCTs1yygTa+JGjZOpKzIZ/It6TZZOO/Q+PmVGuKuIbrznXF64BUFxXaPNy7gO9LnE7vjGdauSQ==} + engines: {node: '>=16', pnpm: '>=8.6.0'} + dependencies: + '@thednp/event-listener': 2.0.4 + '@thednp/shorty': 2.0.0 + dev: true + + /boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 + dev: false + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + + /brfs@2.0.2: + resolution: {integrity: sha512-IrFjVtwu4eTJZyu8w/V2gxU7iLTtcHih67sgEdzrhjLBMHp2uYefUBfdM4k2UvcuWMgV7PQDZHSLeNWnLFKWVQ==} + hasBin: true + dependencies: + quote-stream: 1.0.2 + resolve: 1.22.8 + static-module: 3.0.4 + through2: 2.0.5 + dev: true + + /brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + dependencies: + base64-js: 1.5.1 + dev: true + + /browser-or-node@2.1.1: + resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} + dev: false + + /browser-resolve@1.11.3: + resolution: {integrity: sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==} + dependencies: + resolve: 1.1.7 + dev: true + + /browserify-optional@1.0.1: + resolution: {integrity: sha512-VrhjbZ+Ba5mDiSYEuPelekQMfTbhcA2DhLk2VQWqdcCROWeFqlTcXZ7yfRkXCIl8E+g4gINJYJiRB7WEtfomAQ==} + dependencies: + ast-transform: 0.0.0 + ast-types: 0.7.8 + browser-resolve: 1.11.3 + dev: true + + /browserslist@4.23.0: + resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001587 + electron-to-chromium: 1.4.670 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.23.0) + dev: true + + /bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + dependencies: + fast-json-stable-stringify: 2.1.0 + dev: true + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + dev: true + + /buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: false + + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + + /buffer-equal@0.0.1: + resolution: {integrity: sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==} + engines: {node: '>=0.4.0'} + dev: true + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /buffer-indexof-polyfill@1.0.2: + resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} + engines: {node: '>=0.10'} + dev: false + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /buffers@0.1.1: + resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} + engines: {node: '>=0.2.0'} + dev: false + + /bull@4.12.2: + resolution: {integrity: sha512-WPuc0VCYx+cIVMiZtPwRpWyyJFBrj4/OgKJ6n9Jf4tIw7rQNV+HAKQv15UDkcTvfpGFehvod7Fd1YztbYSJIDQ==} + engines: {node: '>=12'} + dependencies: + cron-parser: 4.9.0 + get-port: 5.1.1 + ioredis: 5.3.2 + lodash: 4.17.21 + msgpackr: 1.10.1 + semver: 7.6.0 + uuid: 8.3.2 + transitivePeerDependencies: + - supports-color + dev: false + + /cache-manager-ioredis-yet@1.2.2: + resolution: {integrity: sha512-o03N/tQxfFONZ1XLGgIxOFHuQQpjpRdnSAL1THG1YWZIVp1JMUfjU3ElSAjFN1LjbJXa55IpC8waG+VEoLUCUw==} + engines: {node: '>= 16.17.0'} + dependencies: + cache-manager: 5.4.0 + ioredis: 5.3.2 + transitivePeerDependencies: + - supports-color + dev: false + + /cache-manager@5.4.0: + resolution: {integrity: sha512-FS7o8vqJosnLpu9rh2gQTo8EOzCRJLF1BJ4XDEUDMqcfvs7SJZs5iuoFTXLauzQ3S5v8sBAST1pCwMaurpyi1A==} + dependencies: + lodash.clonedeep: 4.5.0 + lru-cache: 10.2.0 + promise-coalesce: 1.1.2 + dev: false + + /cachedir@2.3.0: + resolution: {integrity: sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==} + engines: {node: '>=6'} + dev: true + + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.1 + + /callsite@1.0.0: + resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} + dev: true + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + /camel-case@3.0.0: + resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==} + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + dev: false + + /camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + dev: true + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: true + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + /caniuse-lite@1.0.30001587: + resolution: {integrity: sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==} + dev: true + + /chainsaw@0.1.0: + resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} + dependencies: + traverse: 0.3.9 + dev: false + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + /chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: false + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + dev: true + + /character-parser@2.2.0: + resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} + requiresBuild: true + dependencies: + is-regex: 1.1.4 + dev: false + + /chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + dev: true + + /check-disk-space@3.4.0: + resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==} + engines: {node: '>=16'} + dev: false + + /cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + + /cheerio@1.0.0-rc.12: + resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} + engines: {node: '>= 6'} + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + htmlparser2: 8.0.2 + parse5: 7.1.2 + parse5-htmlparser2-tree-adapter: 7.0.0 + + /chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + /chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + requiresBuild: true + dev: false + optional: true + + /chrome-trace-event@1.0.3: + resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} + engines: {node: '>=6.0'} + dev: true + + /ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + /cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + dev: true + + /class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + /class-validator@0.14.1: + resolution: {integrity: sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==} + dependencies: + '@types/validator': 13.11.9 + libphonenumber-js: 1.10.56 + validator: 13.11.0 + + /clean-css@4.2.4: + resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} + engines: {node: '>= 4.0'} + dependencies: + source-map: 0.6.1 + dev: false + + /cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + dev: false + + /cli-cursor@2.1.0: + resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} + engines: {node: '>=4'} + dependencies: + restore-cursor: 2.0.0 + dev: true + + /cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + dependencies: + restore-cursor: 3.1.0 + dev: true + + /cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.0 + dev: false + + /cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + dev: true + + /cli-table3@0.6.3: + resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==} + engines: {node: 10.* || >= 12.*} + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + dev: true + + /cli-width@2.2.1: + resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} + dev: true + + /cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + dev: true + + /cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + dev: true + + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + /clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + dev: true + + /cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + dev: false + + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: true + + /code-block-writer@12.0.0: + resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==} + dev: true + + /collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + dev: false + + /color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + /color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + dev: false + + /colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + dev: true + + /colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + dev: false + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + + /commander@1.1.1: + resolution: {integrity: sha512-71Rod2AhcH3JhkBikVpNd0pA+fWsmAaVoti6OR38T76chA7vE3pSerS0Jor4wDw+tOueD2zLVvFOw5H0Rcj7rA==} + engines: {node: '>= 0.6.x'} + dependencies: + keypress: 0.1.0 + dev: false + + /commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + dev: false + + /commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + dev: true + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + + /commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + dev: false + + /comment-json@4.2.3: + resolution: {integrity: sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==} + engines: {node: '>= 6'} + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + has-own-prop: 2.0.0 + repeat-string: 1.6.1 + dev: true + + /commitizen@4.3.0(@types/node@20.11.18)(typescript@5.3.3): + resolution: {integrity: sha512-H0iNtClNEhT0fotHvGV3E9tDejDeS04sN1veIebsKYGMuGscFaswRoYJKmT3eW85eIJAs0F28bG2+a/9wCOfPw==} + engines: {node: '>= 12'} + hasBin: true + dependencies: + cachedir: 2.3.0 + cz-conventional-changelog: 3.3.0(@types/node@20.11.18)(typescript@5.3.3) + dedent: 0.7.0 + detect-indent: 6.1.0 + find-node-modules: 2.1.3 + find-root: 1.1.0 + fs-extra: 9.1.0 + glob: 7.2.3 + inquirer: 8.2.5 + is-utf8: 0.2.1 + lodash: 4.17.21 + minimist: 1.2.7 + strip-bom: 4.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - '@types/node' + - typescript + dev: true + + /compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + dev: true + + /complex.js@2.1.1: + resolution: {integrity: sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==} + dev: false + + /component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + dev: true + + /compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + dev: true + + /concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + dev: true + + /config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + dev: false + + /connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /consola@2.15.3: + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + + /console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + requiresBuild: true + dev: false + optional: true + + /constantinople@4.0.1: + resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} + requiresBuild: true + dependencies: + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 + dev: false + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /conventional-changelog-angular@5.0.13: + resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} + engines: {node: '>=10'} + dependencies: + compare-func: 2.0.0 + q: 1.5.1 + dev: true + + /conventional-changelog-atom@2.0.8: + resolution: {integrity: sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-codemirror@2.0.8: + resolution: {integrity: sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-config-spec@2.1.0: + resolution: {integrity: sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ==} + dev: true + + /conventional-changelog-conventionalcommits@4.6.3: + resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==} + engines: {node: '>=10'} + dependencies: + compare-func: 2.0.0 + lodash: 4.17.21 + q: 1.5.1 + dev: true + + /conventional-changelog-core@4.2.4: + resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==} + engines: {node: '>=10'} + dependencies: + add-stream: 1.0.0 + conventional-changelog-writer: 5.0.1 + conventional-commits-parser: 3.2.4 + dateformat: 3.0.3 + get-pkg-repo: 4.2.1 + git-raw-commits: 2.0.11 + git-remote-origin-url: 2.0.0 + git-semver-tags: 4.1.1 + lodash: 4.17.21 + normalize-package-data: 3.0.3 + q: 1.5.1 + read-pkg: 3.0.0 + read-pkg-up: 3.0.0 + through2: 4.0.2 + dev: true + + /conventional-changelog-ember@2.0.9: + resolution: {integrity: sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-eslint@3.0.9: + resolution: {integrity: sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-express@2.0.6: + resolution: {integrity: sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-jquery@3.0.11: + resolution: {integrity: sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==} + engines: {node: '>=10'} + dependencies: + q: 1.5.1 + dev: true + + /conventional-changelog-jshint@2.0.9: + resolution: {integrity: sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==} + engines: {node: '>=10'} + dependencies: + compare-func: 2.0.0 + q: 1.5.1 + dev: true + + /conventional-changelog-preset-loader@2.3.4: + resolution: {integrity: sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==} + engines: {node: '>=10'} + dev: true + + /conventional-changelog-writer@5.0.1: + resolution: {integrity: sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==} + engines: {node: '>=10'} + hasBin: true + dependencies: + conventional-commits-filter: 2.0.7 + dateformat: 3.0.3 + handlebars: 4.7.8 + json-stringify-safe: 5.0.1 + lodash: 4.17.21 + meow: 8.1.2 + semver: 6.3.1 + split: 1.0.1 + through2: 4.0.2 + dev: true + + /conventional-changelog@3.1.25: + resolution: {integrity: sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==} + engines: {node: '>=10'} + dependencies: + conventional-changelog-angular: 5.0.13 + conventional-changelog-atom: 2.0.8 + conventional-changelog-codemirror: 2.0.8 + conventional-changelog-conventionalcommits: 4.6.3 + conventional-changelog-core: 4.2.4 + conventional-changelog-ember: 2.0.9 + conventional-changelog-eslint: 3.0.9 + conventional-changelog-express: 2.0.6 + conventional-changelog-jquery: 3.0.11 + conventional-changelog-jshint: 2.0.9 + conventional-changelog-preset-loader: 2.3.4 + dev: true + + /conventional-commit-types@3.0.0: + resolution: {integrity: sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==} + dev: true + + /conventional-commits-filter@2.0.7: + resolution: {integrity: sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==} + engines: {node: '>=10'} + dependencies: + lodash.ismatch: 4.4.0 + modify-values: 1.0.1 + dev: true + + /conventional-commits-parser@3.2.4: + resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==} + engines: {node: '>=10'} + hasBin: true + dependencies: + JSONStream: 1.3.5 + is-text-path: 1.0.1 + lodash: 4.17.21 + meow: 8.1.2 + split2: 3.2.2 + through2: 4.0.2 + dev: true + + /conventional-recommended-bump@6.1.0: + resolution: {integrity: sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + concat-stream: 2.0.0 + conventional-changelog-preset-loader: 2.3.4 + conventional-commits-filter: 2.0.7 + conventional-commits-parser: 3.2.4 + git-raw-commits: 2.0.11 + git-semver-tags: 4.1.1 + meow: 8.1.2 + q: 1.5.1 + dev: true + + /convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + dev: true + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /cookie-signature@1.2.1: + resolution: {integrity: sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==} + engines: {node: '>=6.6.0'} + dev: false + + /cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + + /cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + dev: true + + /copy-to@2.0.1: + resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==} + dev: false + + /core-js-compat@3.36.0: + resolution: {integrity: sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==} + dependencies: + browserslist: 4.23.0 + dev: true + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + /cosmiconfig-typescript-loader@5.0.0(@types/node@20.11.18)(cosmiconfig@8.3.6)(typescript@5.3.3): + resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} + engines: {node: '>=v16'} + requiresBuild: true + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=8.2' + typescript: '>=4' + dependencies: + '@types/node': 20.11.18 + cosmiconfig: 8.3.6(typescript@5.3.3) + jiti: 1.21.0 + typescript: 5.3.3 + dev: true + optional: true + + /cosmiconfig@6.0.0: + resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: false + optional: true + + /cosmiconfig@8.3.6(typescript@5.3.3): + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + typescript: 5.3.3 + dev: true + + /crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + dev: false + + /crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + dev: false + + /crc32@0.2.2: + resolution: {integrity: sha512-PFZEGbDUeoNbL2GHIEpJRQGheXReDody/9axKTxhXtQqIL443wnNigtVZO9iuCIMPApKZRv7k2xr8euXHqNxQQ==} + engines: {node: '>= 0.4.0'} + hasBin: true + dev: false + + /create-jest@29.7.0(@types/node@20.11.18)(ts-node@10.9.2): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.11.18)(ts-node@10.9.2) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + /cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + dependencies: + luxon: 3.4.4 + dev: false + + /cron@3.1.6: + resolution: {integrity: sha512-cvFiQCeVzsA+QPM6fhjBtlKGij7tLLISnTSvFxVdnFGLdz+ZdXN37kNe0i2gefmdD17XuZA6n2uPVwzl4FxW/w==} + dependencies: + '@types/luxon': 3.3.8 + luxon: 3.4.4 + dev: false + + /cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + dependencies: + cross-spawn: 7.0.3 + dev: true + + /cross-spawn@6.0.5: + resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} + engines: {node: '>=4.8'} + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + dev: false + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + /crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + + /css-inline@0.11.2: + resolution: {integrity: sha512-c/oie5Yqa2lVRwUO7A8nd3c3r0x7yE6MQH2PPB/R1LaUb6ohZD7vNXj23fod5y4QNsNhsQi98/AWfUwo1K6R7g==} + dev: false + + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + /cz-conventional-changelog@3.3.0(@types/node@20.11.18)(typescript@5.3.3): + resolution: {integrity: sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==} + engines: {node: '>= 10'} + dependencies: + chalk: 2.4.2 + commitizen: 4.3.0(@types/node@20.11.18)(typescript@5.3.3) + conventional-commit-types: 3.0.0 + lodash.map: 4.6.0 + longest: 2.0.1 + word-wrap: 1.2.5 + optionalDependencies: + '@commitlint/load': 18.6.1(@types/node@20.11.18)(typescript@5.3.3) + transitivePeerDependencies: + - '@types/node' + - typescript + dev: true + + /cz-customizable@7.0.0: + resolution: {integrity: sha512-pQKkGSm+8SY9VY/yeJqDOla1MjrGaG7WG4EYLLEV4VNctGO7WdzdGtWEr2ydKSkrpmTs7f8fmBksg/FaTrUAyw==} + hasBin: true + dependencies: + editor: 1.0.0 + find-config: 1.0.0 + inquirer: 6.5.2 + lodash: 4.17.21 + temp: 0.9.4 + word-wrap: 1.2.5 + dev: true + + /d@1.0.1: + resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==} + dependencies: + es5-ext: 0.10.62 + type: 1.2.0 + dev: true + + /dargs@7.0.0: + resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} + engines: {node: '>=8'} + dev: true + + /dash-ast@2.0.1: + resolution: {integrity: sha512-5TXltWJGc+RdnabUGzhRae1TRq6m4gr+3K2wQX0is5/F2yS6MJXJvLyI3ErAnsAXuJoGqvfVD5icRgim07DrxQ==} + dev: true + + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.23.9 + dev: false + + /dateformat@3.0.3: + resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} + dev: true + + /dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + + /debug@3.1.0: + resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + + /decache@4.6.2: + resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==} + dependencies: + callsite: 1.0.0 + dev: true + + /decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + dev: true + + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: false + + /decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + dev: false + + /dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + dev: true + + /dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + dev: true + + /deep-equal@1.1.2: + resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==} + engines: {node: '>= 0.4'} + dependencies: + is-arguments: 1.1.1 + is-date-object: 1.0.5 + is-regex: 1.1.4 + object-is: 1.1.5 + object-keys: 1.1.1 + regexp.prototype.flags: 1.5.2 + dev: true + + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: false + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + /default-user-agent@1.0.0: + resolution: {integrity: sha512-bDF7bg6OSNcSwFWPu4zYKpVkJZQYVrAANMYB8bc9Szem1D0yKdm4sa/rOCs2aC9+2GMqQ7KnwtZRvDhmLF0dXw==} + engines: {node: '>= 0.10.0'} + dependencies: + os-name: 1.0.3 + dev: false + + /defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + dependencies: + clone: 1.0.4 + dev: true + + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + /define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + dev: true + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + /delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + requiresBuild: true + dev: false + optional: true + + /denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dev: false + + /depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + dev: true + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + /detect-file@1.0.0: + resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} + engines: {node: '>=0.10.0'} + dev: true + + /detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + /detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + requiresBuild: true + dev: false + optional: true + + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + /detect-node@2.0.4: + resolution: {integrity: sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==} + dev: false + + /detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + dev: false + + /dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + dev: true + + /dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + dev: true + + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + /digest-header@1.1.0: + resolution: {integrity: sha512-glXVh42vz40yZb9Cq2oMOt70FIoWiv+vxNvdKdU8CwjLad25qHM3trLxhl9bVjdr6WaslIXhWpn0NO8T/67Qjg==} + engines: {node: '>= 8.0.0'} + dev: false + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /display-notification@2.0.0: + resolution: {integrity: sha512-TdmtlAcdqy1NU+j7zlkDdMnCL878zriLaBmoD9quOoq1ySSSGv03l0hXK5CvIFZlIfFI/hizqdQuW+Num7xuhw==} + engines: {node: '>=4'} + dependencies: + escape-string-applescript: 1.0.0 + run-applescript: 3.2.0 + dev: false + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctypes@1.1.0: + resolution: {integrity: sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==} + requiresBuild: true + dev: false + + /dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + dev: false + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + /domhandler@3.3.0: + resolution: {integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: false + + /domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: false + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + + /domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + dev: false + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + /dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + dependencies: + is-obj: 2.0.0 + dev: true + + /dot@2.0.0-beta.1: + resolution: {integrity: sha512-kxM7fSnNQTXOmaeGuBSXM8O3fEsBb7XSDBllkGbRwa0lJSJTxxDE/4eSNGLKZUmlFw0f1vJ5qSV2BljrgQtgIA==} + dev: true + + /dotenv-expand@10.0.0: + resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} + engines: {node: '>=12'} + dev: false + + /dotenv@16.4.1: + resolution: {integrity: sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==} + engines: {node: '>=12'} + dev: false + + /dotenv@16.4.4: + resolution: {integrity: sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg==} + engines: {node: '>=12'} + dev: false + + /dotgitignore@2.1.0: + resolution: {integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==} + engines: {node: '>=6'} + dependencies: + find-up: 3.0.0 + minimatch: 3.1.2 + dev: true + + /duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + dependencies: + readable-stream: 2.3.8 + + /duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + dev: true + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /editor@1.0.0: + resolution: {integrity: sha512-SoRmbGStwNYHgKfjOrX2L0mUvp9bUVv0uPppZSOMAntEbcFtoC3MKF5b3T6HQPXKIV+QGY3xPO3JK5it5lVkuw==} + dev: true + + /editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.6.0 + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + /ejs@3.1.9: + resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + jake: 10.8.7 + dev: false + optional: true + + /electron-to-chromium@1.4.670: + resolution: {integrity: sha512-hcijYOWjOtjKrKPtNA6tuLlA/bTLO3heFG8pQA6mLpq7dRydSWicXova5lyxDzp1iVJaYhK7J2OQlGE52KYn7A==} + dev: true + + /emitter-component@1.1.2: + resolution: {integrity: sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==} + dev: true + + /emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + /enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + dev: false + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + /encoding-japanese@2.0.0: + resolution: {integrity: sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==} + engines: {node: '>=8.10.0'} + dev: false + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + + /engine.io-parser@5.2.2: + resolution: {integrity: sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==} + engines: {node: '>=10.0.0'} + + /engine.io@6.5.4: + resolution: {integrity: sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==} + engines: {node: '>=10.2.0'} + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.17 + '@types/node': 20.11.18 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.4.2 + cors: 2.8.5 + debug: 4.3.4 + engine.io-parser: 5.2.2 + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + /enhanced-resolve@5.15.0: + resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + dev: true + + /entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + dev: false + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + + /error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + dependencies: + stackframe: 1.3.4 + dev: false + + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + /es-module-lexer@1.4.1: + resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} + dev: true + + /es5-ext@0.10.62: + resolution: {integrity: sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==} + engines: {node: '>=0.10'} + requiresBuild: true + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.3 + next-tick: 1.1.0 + dev: true + + /es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + dependencies: + d: 1.0.1 + es5-ext: 0.10.62 + es6-symbol: 3.1.3 + dev: true + + /es6-map@0.1.5: + resolution: {integrity: sha512-mz3UqCh0uPCIqsw1SSAkB/p0rOzF/M0V++vyN7JqlPtSW/VsYgQBvVvqMLmfBuyMzTpLnNqi6JmcSizs4jy19A==} + dependencies: + d: 1.0.1 + es5-ext: 0.10.62 + es6-iterator: 2.0.3 + es6-set: 0.1.6 + es6-symbol: 3.1.3 + event-emitter: 0.3.5 + dev: true + + /es6-set@0.1.6: + resolution: {integrity: sha512-TE3LgGLDIBX332jq3ypv6bcOpkLO0AslAQo7p2VqX/1N46YNsvIWgvjojjSEnWEGWMhr1qUbYeTSir5J6mFHOw==} + engines: {node: '>=0.12'} + dependencies: + d: 1.0.1 + es5-ext: 0.10.62 + es6-iterator: 2.0.3 + es6-symbol: 3.1.3 + event-emitter: 0.3.5 + type: 2.7.2 + dev: true + + /es6-shim@0.35.8: + resolution: {integrity: sha512-Twf7I2v4/1tLoIXMT8HlqaBSS5H2wQTs2wx3MNYCI8K1R1/clXyCazrcVCPm/FuO9cyV8+leEaZOWD5C253NDg==} + dev: true + + /es6-symbol@3.1.3: + resolution: {integrity: sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==} + dependencies: + d: 1.0.1 + ext: 1.7.0 + dev: true + + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + /escape-goat@3.0.0: + resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==} + engines: {node: '>=10'} + dev: false + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + /escape-latex@1.2.0: + resolution: {integrity: sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==} + dev: false + + /escape-string-applescript@1.0.0: + resolution: {integrity: sha512-4/hFwoYaC6TkpDn9A3pTC52zQPArFeXuIfhUtCGYdauTzXVP9H3BDr3oO/QzQehMpLDC7srvYgfwvImPFGfvBA==} + engines: {node: '>=0.10.0'} + dev: false + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + dev: true + + /escodegen@1.14.3: + resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==} + engines: {node: '>=4.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 4.3.0 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + dev: true + + /escodegen@1.2.0: + resolution: {integrity: sha512-yLy3Cc+zAC0WSmoT2fig3J87TpQ8UaZGx8ahCAs9FL8qNbyV7CVyPKS74DG4bsHiL5ew9sxdYx131OkBQMFnvA==} + engines: {node: '>=0.4.0'} + hasBin: true + dependencies: + esprima: 1.0.4 + estraverse: 1.5.1 + esutils: 1.0.0 + optionalDependencies: + source-map: 0.1.43 + dev: true + + /escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + dev: true + + /eslint-config-prettier@8.10.0(eslint@8.57.0): + resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.57.0 + dev: true + + /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0)(eslint@8.57.0)(prettier@3.2.5): + resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + eslint: '>=7.28.0' + eslint-config-prettier: '*' + prettier: '>=2.0.0' + peerDependenciesMeta: + eslint-config-prettier: + optional: true + dependencies: + eslint: 8.57.0 + eslint-config-prettier: 8.10.0(eslint@8.57.0) + prettier: 3.2.5 + prettier-linter-helpers: 1.0.0 + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) + eslint-visitor-keys: 3.4.3 + dev: true + + /esprima@1.0.4: + resolution: {integrity: sha512-rp5dMKN8zEs9dfi9g0X1ClLmV//WRyk/R15mppFNICIFRG5P92VP7Z04p8pk++gABo9W2tY+kHyu6P1mEHgmTA==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /esprima@1.2.5: + resolution: {integrity: sha512-S9VbPDU0adFErpDai3qDkjq8+G05ONtKzcyNrPKg/ZKa+tf879nX2KexNU95b31UoTJjRLInNBHHHjFPoCd7lQ==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: false + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@1.5.1: + resolution: {integrity: sha512-FpCjJDfmo3vsc/1zKSeqR5k42tcIhxFIlvq+h9j0fO2q/h2uLKyweq7rYJ+0CoVvrGQOxIS5wyBrW/+vF58BUQ==} + engines: {node: '>=0.4.0'} + dev: true + + /estraverse@1.9.3: + resolution: {integrity: sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==} + engines: {node: '>=0.10.0'} + dev: false + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /estree-is-function@1.0.0: + resolution: {integrity: sha512-nSCWn1jkSq2QAtkaVLJZY2ezwcFO161HVc174zL1KPW3RJ+O6C3eJb8Nx7OXzvhoEv+nLgSR1g71oWUHUDTrJA==} + dev: true + + /esutils@1.0.0: + resolution: {integrity: sha512-x/iYH53X3quDwfHRz4y8rn4XcEwwCJeWsul9pF1zldMbGtgOtMNBEOuYWwB1EQlK2LRa1fev3YAgym/RElp5Cg==} + engines: {node: '>=0.10.0'} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: true + + /event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + dependencies: + d: 1.0.1 + es5-ext: 0.10.62 + dev: true + + /event-stream@4.0.1: + resolution: {integrity: sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==} + dependencies: + duplexer: 0.1.2 + from: 0.1.7 + map-stream: 0.0.7 + pause-stream: 0.0.11 + split: 1.0.1 + stream-combiner: 0.2.2 + through: 2.3.8 + dev: true + + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: false + + /eventemitter2@6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} + dev: false + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + /exceljs@4.4.0: + resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==} + engines: {node: '>=8.3.0'} + dependencies: + archiver: 5.3.2 + dayjs: 1.11.10 + fast-csv: 4.3.6 + jszip: 3.10.1 + readable-stream: 3.6.2 + saxes: 5.0.1 + tmp: 0.2.3 + unzipper: 0.10.14 + uuid: 8.3.2 + dev: false + + /execa@0.10.0: + resolution: {integrity: sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==} + engines: {node: '>=4'} + dependencies: + cross-spawn: 6.0.5 + get-stream: 3.0.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + dev: false + + /execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + dev: true + + /expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + dependencies: + homedir-polyfill: 1.0.3 + dev: true + + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + dev: true + + /ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + dependencies: + type: 2.7.2 + dev: true + + /extend-object@1.0.0: + resolution: {integrity: sha512-0dHDIXC7y7LDmCh/lp1oYkmv73K25AMugQI07r8eFopkW6f7Ufn1q+ETMsJjnV9Am14SlElkqy3O92r6xEaxPw==} + dev: false + + /extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + dependencies: + is-extendable: 0.1.1 + dev: false + + /external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + dev: true + + /fancy-log@2.0.0: + resolution: {integrity: sha512-9CzxZbACXMUXW13tS0tI8XsGGmxWzO2DmYrGuBJOJ8k8q2K7hwfJA5qHjuPPe8wtsco33YR9wc+Rlr5wYFvhSA==} + engines: {node: '>=10.13.0'} + dependencies: + color-support: 1.1.3 + dev: true + + /fast-content-type-parse@1.1.0: + resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} + dev: false + + /fast-csv@4.3.6: + resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==} + engines: {node: '>=10.0.0'} + dependencies: + '@fast-csv/format': 4.3.5 + '@fast-csv/parse': 4.3.6 + dev: false + + /fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + dev: false + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-json-stringify@5.12.0: + resolution: {integrity: sha512-7Nnm9UPa7SfHRbHVA1kJQrGXCRzB7LMlAAqHXQFkEQqueJm1V8owm0FsE/2Do55/4CcdhwiLQERaKomOnKQkyA==} + dependencies: + '@fastify/merge-json-schemas': 0.1.1 + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + fast-deep-equal: 3.1.3 + fast-uri: 2.3.0 + json-schema-ref-resolver: 1.0.1 + rfdc: 1.3.1 + dev: false + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + dependencies: + fast-decode-uri-component: 1.0.1 + dev: false + + /fast-redact@3.3.0: + resolution: {integrity: sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==} + engines: {node: '>=6'} + dev: false + + /fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + /fast-uri@2.3.0: + resolution: {integrity: sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==} + dev: false + + /fast-xml-parser@4.3.6: + resolution: {integrity: sha512-M2SovcRxD4+vC493Uc2GZVcZaj66CCJhWurC4viynVSTvrpErCShNcDz1lAho6n9REQKvL/ll4A4/fw6Y9z8nw==} + hasBin: true + dependencies: + strnum: 1.0.5 + dev: false + + /fastify-plugin@4.5.1: + resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} + dev: false + + /fastify@4.26.0: + resolution: {integrity: sha512-Fq/7ziWKc6pYLYLIlCRaqJqEVTIZ5tZYfcW/mDK2AQ9v/sqjGFpj0On0/7hU50kbPVjLO4de+larPA1WwPZSfw==} + dependencies: + '@fastify/ajv-compiler': 3.5.0 + '@fastify/error': 3.4.1 + '@fastify/fast-json-stringify-compiler': 4.3.0 + abstract-logging: 2.0.1 + avvio: 8.3.0 + fast-content-type-parse: 1.1.0 + fast-json-stringify: 5.12.0 + find-my-way: 8.1.0 + light-my-request: 5.11.0 + pino: 8.18.0 + process-warning: 3.0.0 + proxy-addr: 2.0.7 + rfdc: 1.3.1 + secure-json-parse: 2.7.0 + semver: 7.6.0 + toad-cache: 3.7.0 + transitivePeerDependencies: + - supports-color + dev: false + + /fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + dependencies: + reusify: 1.0.4 + + /faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + dependencies: + websocket-driver: 0.7.4 + dev: true + + /fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + dependencies: + bser: 2.1.1 + dev: true + + /fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + dev: false + + /figures@2.0.0: + resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} + engines: {node: '>=4'} + dependencies: + escape-string-regexp: 1.0.5 + dev: true + + /figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + dependencies: + escape-string-regexp: 1.0.5 + dev: true + + /figures@5.0.0: + resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==} + engines: {node: '>=14'} + dependencies: + escape-string-regexp: 5.0.0 + is-unicode-supported: 1.3.0 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.2.0 + dev: true + + /file-stream-rotator@0.6.1: + resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} + dependencies: + moment: 2.30.1 + dev: false + + /filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + requiresBuild: true + dependencies: + minimatch: 5.1.6 + dev: false + optional: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + + /filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + dev: false + + /finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /find-config@1.0.0: + resolution: {integrity: sha512-Z+suHH+7LSE40WfUeZPIxSxypCWvrzdVc60xAjUShZeT5eMWM0/FQUduq3HjluyfAHWvC/aOBkT1pTZktyF/jg==} + engines: {node: '>= 0.12'} + dependencies: + user-home: 2.0.0 + dev: true + + /find-my-way@8.1.0: + resolution: {integrity: sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==} + engines: {node: '>=14'} + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 2.0.0 + dev: false + + /find-node-modules@2.1.3: + resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==} + dependencies: + findup-sync: 4.0.0 + merge: 2.1.1 + dev: true + + /find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + dev: true + + /find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + dependencies: + locate-path: 2.0.0 + dev: true + + /find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + dependencies: + locate-path: 3.0.0 + dev: true + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /findup-sync@4.0.0: + resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==} + engines: {node: '>= 8'} + dependencies: + detect-file: 1.0.0 + is-glob: 4.0.3 + micromatch: 4.0.5 + resolve-dir: 1.0.1 + dev: true + + /fixpack@4.0.0: + resolution: {integrity: sha512-5SM1+H2CcuJ3gGEwTiVo/+nd/hYpNj9Ch3iMDOQ58ndY+VGQ2QdvaUTkd3otjZvYnd/8LF/HkJ5cx7PBq0orCQ==} + hasBin: true + dependencies: + alce: 1.2.0 + chalk: 3.0.0 + detect-indent: 6.1.0 + detect-newline: 3.1.0 + extend-object: 1.0.0 + rc: 1.2.8 + dev: false + + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.9 + keyv: 4.5.4 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + dev: true + + /fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + dev: false + + /follow-redirects@1.15.5: + resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: false + + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + /fork-ts-checker-webpack-plugin@9.0.2(typescript@5.3.3)(webpack@5.90.1): + resolution: {integrity: sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==} + engines: {node: '>=12.13.0', yarn: '>=1.0.0'} + peerDependencies: + typescript: '>3.6.0' + webpack: ^5.11.0 + dependencies: + '@babel/code-frame': 7.23.5 + chalk: 4.1.2 + chokidar: 3.6.0 + cosmiconfig: 8.3.6(typescript@5.3.3) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.6.0 + tapable: 2.2.1 + typescript: 5.3.3 + webpack: 5.90.1 + dev: true + + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + /formidable@2.1.2: + resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} + dependencies: + dezalgo: 1.0.4 + hexoid: 1.0.0 + once: 1.4.0 + qs: 6.11.2 + dev: true + + /formstream@1.3.1: + resolution: {integrity: sha512-FkW++ub+VbE5dpwukJVDizNWhSgp8FhmhI65pF7BZSVStBqe6Wgxe2Z9/Vhsn7l7nXCPwP+G1cyYlX8VwWOf0g==} + dependencies: + destroy: 1.2.0 + mime: 2.6.0 + pause-stream: 0.0.11 + dev: false + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fraction.js@4.3.4: + resolution: {integrity: sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q==} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: true + + /from@0.1.7: + resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} + dev: true + + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: false + + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + + /fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + + /fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + + /fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + requiresBuild: true + dependencies: + minipass: 3.3.6 + dev: false + optional: true + + /fs-monkey@1.0.5: + resolution: {integrity: sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==} + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + optional: true + + /fstream@1.0.12: + resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} + engines: {node: '>=0.6'} + dependencies: + graceful-fs: 4.2.11 + inherits: 2.0.4 + mkdirp: 0.5.6 + rimraf: 2.6.3 + dev: false + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + requiresBuild: true + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + dev: false + optional: true + + /generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + dependencies: + is-property: 1.0.2 + dev: false + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-assigned-identifiers@1.2.0: + resolution: {integrity: sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==} + dev: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.1 + + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + dev: true + + /get-pkg-repo@4.2.1: + resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==} + engines: {node: '>=6.9.0'} + hasBin: true + dependencies: + '@hutson/parse-repository-url': 3.0.2 + hosted-git-info: 4.1.0 + through2: 2.0.5 + yargs: 16.2.0 + dev: true + + /get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} + dev: false + + /get-stream@3.0.0: + resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} + engines: {node: '>=4'} + dev: false + + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.0 + dev: true + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true + + /git-raw-commits@2.0.11: + resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} + engines: {node: '>=10'} + hasBin: true + dependencies: + dargs: 7.0.0 + lodash: 4.17.21 + meow: 8.1.2 + split2: 3.2.2 + through2: 4.0.2 + dev: true + + /git-remote-origin-url@2.0.0: + resolution: {integrity: sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==} + engines: {node: '>=4'} + dependencies: + gitconfiglocal: 1.0.0 + pify: 2.3.0 + dev: true + + /git-semver-tags@4.1.1: + resolution: {integrity: sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + meow: 8.1.2 + semver: 6.3.1 + dev: true + + /gitconfiglocal@1.0.0: + resolution: {integrity: sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==} + dependencies: + ini: 1.3.8 + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: true + + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + dev: false + + /glob@9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.4 + minipass: 4.2.8 + path-scurry: 1.10.1 + dev: true + + /global-dirs@0.1.1: + resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} + engines: {node: '>=4'} + requiresBuild: true + dependencies: + ini: 1.3.8 + dev: true + optional: true + + /global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + dev: true + + /global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.4 + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /hammerjs@2.0.8: + resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} + engines: {node: '>=0.8.0'} + dev: true + + /handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.17.4 + + /hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + /has-own-prop@2.0.0: + resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + + /has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + requiresBuild: true + dev: false + optional: true + + /has@1.0.4: + resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} + engines: {node: '>= 0.4.0'} + dev: true + + /hasown@2.0.1: + resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: false + + /helmet@7.1.0: + resolution: {integrity: sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==} + engines: {node: '>=16.0.0'} + dev: false + + /hexoid@1.0.0: + resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} + engines: {node: '>=8'} + dev: true + + /highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + dev: false + + /homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + dependencies: + parse-passwd: 1.0.0 + dev: true + + /hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + dev: true + + /hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + dependencies: + lru-cache: 6.0.0 + dev: true + + /html-entities@2.4.0: + resolution: {integrity: sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==} + dev: true + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + + /html-minifier@4.0.0: + resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==} + engines: {node: '>=6'} + hasBin: true + dependencies: + camel-case: 3.0.0 + clean-css: 4.2.4 + commander: 2.20.3 + he: 1.2.0 + param-case: 2.1.1 + relateurl: 0.2.7 + uglify-js: 3.17.4 + dev: false + + /html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + dev: false + + /htmlparser2@5.0.1: + resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==} + dependencies: + domelementtype: 2.3.0 + domhandler: 3.3.0 + domutils: 2.8.0 + entities: 2.2.0 + dev: false + + /htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + + /http-auth-connect@1.0.6: + resolution: {integrity: sha512-yaO0QSCPqGCjPrl3qEEHjJP+lwZ6gMpXLuCBE06eWwcXomkI5TARtu0kxf9teFuBj6iaV3Ybr15jaWUvbzNzHw==} + engines: {node: '>=8'} + dev: true + + /http-auth@4.1.9: + resolution: {integrity: sha512-kvPYxNGc9EKGTXvOMnTBQw2RZfuiSihK/mLw/a4pbtRueTE45S55Lw/3k5CktIf7Ak0veMKEIteDj4YkNmCzmQ==} + engines: {node: '>=8'} + dependencies: + apache-crypt: 1.2.6 + apache-md5: 1.1.8 + bcryptjs: 2.4.3 + uuid: 8.3.2 + dev: true + + /http-errors@1.6.3: + resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} + engines: {node: '>= 0.6'} + dependencies: + depd: 1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.0 + statuses: 1.5.0 + dev: true + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + /http-parser-js@0.5.8: + resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} + dev: true + + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + requiresBuild: true + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + optional: true + + /human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + dev: true + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: true + + /humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + dependencies: + ms: 2.1.3 + dev: false + + /husky@8.0.3: + resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /i18next@23.8.2: + resolution: {integrity: sha512-Z84zyEangrlERm0ZugVy4bIt485e/H8VecGUZkZWrH7BDePG6jT73QdL9EA1tRTTVVMpry/MgWIP1FjEn0DRXA==} + dependencies: + '@babel/runtime': 7.23.9 + dev: true + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + /ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + dev: true + + /immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + dev: false + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + /import-local@3.1.0: + resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + /inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + /inquirer@6.5.2: + resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==} + engines: {node: '>=6.0.0'} + dependencies: + ansi-escapes: 3.2.0 + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-width: 2.2.1 + external-editor: 3.1.0 + figures: 2.0.0 + lodash: 4.17.21 + mute-stream: 0.0.7 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 2.1.1 + strip-ansi: 5.2.0 + through: 2.3.8 + dev: true + + /inquirer@8.2.5: + resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} + engines: {node: '>=12.0.0'} + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + dev: true + + /inquirer@8.2.6: + resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} + engines: {node: '>=12.0.0'} + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + dev: true + + /inquirer@9.2.12: + resolution: {integrity: sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==} + engines: {node: '>=14.18.0'} + dependencies: + '@ljharb/through': 2.3.12 + ansi-escapes: 4.3.2 + chalk: 5.3.0 + cli-cursor: 3.1.0 + cli-width: 4.1.0 + external-editor: 3.1.0 + figures: 5.0.0 + lodash: 4.17.21 + mute-stream: 1.0.0 + ora: 5.4.1 + run-async: 3.0.0 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: true + + /interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + dev: true + + /ioredis@5.3.2: + resolution: {integrity: sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==} + engines: {node: '>=12.22.0'} + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.2 + debug: 4.3.4 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /ip@1.1.8: + resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==} + dev: false + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /ipaddr.js@2.1.0: + resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==} + engines: {node: '>= 10'} + dev: false + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + /is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + dev: false + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: false + + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + dependencies: + hasown: 2.0.1 + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + /is-expression@4.0.0: + resolution: {integrity: sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==} + requiresBuild: true + dependencies: + acorn: 7.4.1 + object-assign: 4.1.1 + dev: false + + /is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + dev: false + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + /is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + /is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + dev: true + + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: false + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + + /is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + /is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + requiresBuild: true + dev: false + + /is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + dev: false + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + /is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + dev: false + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + /is-text-path@1.0.1: + resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==} + engines: {node: '>=0.10.0'} + dependencies: + text-extensions: 1.9.0 + dev: true + + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.15 + dev: false + + /is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: true + + /is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + dev: true + + /is-utf8@0.2.1: + resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==} + dev: true + + /is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + dev: true + + /is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + dependencies: + is-docker: 2.2.1 + + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.23.9 + '@babel/parser': 7.23.9 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-instrument@6.0.1: + resolution: {integrity: sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.23.9 + '@babel/parser': 7.23.9 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.1.6: + resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + + /iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + /jake@10.8.7: + resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==} + engines: {node: '>=10'} + hasBin: true + requiresBuild: true + dependencies: + async: 3.2.5 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + dev: false + optional: true + + /javascript-natural-sort@0.7.1: + resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} + dev: false + + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + dev: true + + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.18 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.1 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.0.4 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-cli@29.7.0(@types/node@20.11.18)(ts-node@10.9.2): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.11.18)(ts-node@10.9.2) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@20.11.18)(ts-node@10.9.2) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /jest-config@29.7.0(@types/node@20.11.18)(ts-node@10.9.2): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.23.9 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.18 + babel-jest: 29.7.0(@babel/core@7.23.9) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + ts-node: 10.9.2(@types/node@20.11.18)(typescript@5.3.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + dev: true + + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + dev: true + + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.18 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 20.11.18 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.23.5 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: true + + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.11.18 + jest-util: 29.7.0 + dev: true + + /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.7.0 + dev: true + + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.8 + resolve.exports: 2.0.2 + slash: 3.0.0 + dev: true + + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.18 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.18 + chalk: 4.1.2 + cjs-module-lexer: 1.2.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.23.9 + '@babel/generator': 7.23.6 + '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.9) + '@babel/types': 7.23.9 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.9) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.11.18 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: true + + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + dev: true + + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.18 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + dev: true + + /jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 20.11.18 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 20.11.18 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /jest@29.7.0(@types/node@20.11.18)(ts-node@10.9.2): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@20.11.18)(ts-node@10.9.2) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + + /jiti@1.21.0: + resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /js-beautify@1.14.11: + resolution: {integrity: sha512-rPogWqAfoYh1Ryqqh2agUpVfbxAhbjuN1SmU86dskQUKouRiggUTCO4+2ym9UPXllc2WAp0J+T5qxn7Um3lCdw==} + engines: {node: '>=14'} + hasBin: true + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.3.10 + nopt: 7.2.0 + dev: false + + /js-stringify@1.0.2: + resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==} + requiresBuild: true + dev: false + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + + /jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + /json-schema-ref-resolver@1.0.1: + resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} + dependencies: + fast-deep-equal: 3.1.3 + dev: false + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json-stream@1.0.0: + resolution: {integrity: sha512-H/ZGY0nIAg3QcOwE1QN/rK/Fa7gJn7Ii5obwp6zyPO4xiPNwpIMjqy2gwjBEGqzkF/vSWEIBQCBuN19hYiL6Qg==} + dev: false + + /json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsonc-parser@3.1.0: + resolution: {integrity: sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==} + dev: true + + /jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + + /jsonc-parser@3.2.1: + resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + dev: true + + /jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.6.0 + dev: false + + /jstransformer@1.0.0: + resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} + requiresBuild: true + dependencies: + is-promise: 2.2.2 + promise: 7.3.1 + dev: false + + /jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + dev: false + + /juice@9.1.0: + resolution: {integrity: sha512-odblShmPrUoHUwRuC8EmLji5bPP2MLO1GL+gt4XU3tT2ECmbSrrMjtMQaqg3wgMFP2zvUzdPZGfxc5Trk3Z+fQ==} + engines: {node: '>=10.0.0'} + hasBin: true + dependencies: + cheerio: 1.0.0-rc.12 + commander: 6.2.1 + mensch: 0.3.4 + slick: 1.12.2 + web-resource-inliner: 6.0.1 + transitivePeerDependencies: + - encoding + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + + /keycharm@0.2.0: + resolution: {integrity: sha512-i/XBRTiLqRConPKioy2oq45vbv04e8x59b0mnsIRQM+7Ec/8BC7UcL5pnC4FMeGb8KwG7q4wOMw7CtNZf5tiIg==} + dev: true + + /keypress@0.1.0: + resolution: {integrity: sha512-x0yf9PL/nx9Nw9oLL8ZVErFAk85/lslwEP7Vz7s5SI1ODXZIgit3C5qyWjw4DxOuO/3Hb4866SQh28a1V1d+WA==} + dev: false + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: true + + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: true + + /kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + dev: false + + /lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + dependencies: + readable-stream: 2.3.8 + dev: false + + /leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + dev: false + + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + dev: true + + /levn@0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /libbase64@1.2.1: + resolution: {integrity: sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==} + dev: false + + /libmime@5.2.0: + resolution: {integrity: sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw==} + dependencies: + encoding-japanese: 2.0.0 + iconv-lite: 0.6.3 + libbase64: 1.2.1 + libqp: 2.0.1 + dev: false + + /libmime@5.2.1: + resolution: {integrity: sha512-A0z9O4+5q+ZTj7QwNe/Juy1KARNb4WaviO4mYeFC4b8dBT2EEqK2pkM+GC8MVnkOjqhl5nYQxRgnPYRRTNmuSQ==} + dependencies: + encoding-japanese: 2.0.0 + iconv-lite: 0.6.3 + libbase64: 1.2.1 + libqp: 2.0.1 + dev: false + + /libphonenumber-js@1.10.56: + resolution: {integrity: sha512-d0GdKshNnyfl5gM7kZ9rXjGiAbxT/zCXp0k+EAzh8H4zrb2R7GXtMCrULrX7UQxtfx6CLy/vz/lomvW79FAFdA==} + + /libqp@2.0.1: + resolution: {integrity: sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg==} + dev: false + + /lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + dependencies: + immediate: 3.0.6 + dev: false + + /light-my-request@5.11.0: + resolution: {integrity: sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA==} + dependencies: + cookie: 0.5.0 + process-warning: 2.3.2 + set-cookie-parser: 2.6.0 + dev: false + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + /linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + dependencies: + uc.micro: 2.0.0 + dev: false + + /listenercount@1.0.1: + resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} + dev: false + + /load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + dev: true + + /loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + dev: true + + /locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + dev: true + + /locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + dev: false + + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: true + + /lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + dev: false + + /lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + dev: false + + /lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + dev: false + + /lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + dev: false + + /lodash.groupby@4.6.0: + resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} + dev: false + + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + + /lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + dev: false + + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + dev: false + + /lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + dev: false + + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.ismatch@4.4.0: + resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} + dev: true + + /lodash.isnil@4.0.0: + resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==} + dev: false + + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + + /lodash.isundefined@3.0.1: + resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==} + dev: false + + /lodash.map@4.6.0: + resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==} + dev: true + + /lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + requiresBuild: true + dev: true + optional: true + + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + + /lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + dev: false + + /lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + requiresBuild: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + /log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: true + + /logform@2.6.0: + resolution: {integrity: sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.4.3 + triple-beam: 1.4.1 + dev: false + + /loglevel-plugin-prefix@0.8.4: + resolution: {integrity: sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==} + dev: true + + /loglevel@1.9.1: + resolution: {integrity: sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==} + engines: {node: '>= 0.6.0'} + dev: true + + /long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + dev: false + + /longest@2.0.1: + resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==} + engines: {node: '>=0.10.0'} + dev: true + + /lower-case@1.1.4: + resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} + dev: false + + /lru-cache@10.2.0: + resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} + engines: {node: 14 || >=16.14} + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + + /lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + dev: false + + /lru-cache@8.0.5: + resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==} + engines: {node: '>=16.14'} + dev: false + + /lunr@2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + dev: true + + /luxon@3.4.4: + resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} + engines: {node: '>=12'} + dev: false + + /macos-release@2.5.1: + resolution: {integrity: sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==} + engines: {node: '>=6'} + dev: true + + /magic-string@0.25.1: + resolution: {integrity: sha512-sCuTz6pYom8Rlt4ISPFn6wuFodbKMIHUMv4Qko9P17dpxb7s52KJTmRuZZqHdGmLCK9AOcDare039nRIcfdkEg==} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /magic-string@0.26.2: + resolution: {integrity: sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==} + engines: {node: '>=12'} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /magic-string@0.30.5: + resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /mailparser@3.6.7: + resolution: {integrity: sha512-/3x8HW70DNehw+3vdOPKdlLuxOHoWcGB5jfx5vJ5XUbY9/2jUJbrrhda5Si8Dj/3w08U0y5uGAkqs5+SPTPKoA==} + dependencies: + encoding-japanese: 2.0.0 + he: 1.2.0 + html-to-text: 9.0.5 + iconv-lite: 0.6.3 + libmime: 5.2.1 + linkify-it: 5.0.0 + mailsplit: 5.4.0 + nodemailer: 6.9.9 + tlds: 1.248.0 + dev: false + + /mailsplit@5.4.0: + resolution: {integrity: sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA==} + dependencies: + libbase64: 1.2.1 + libmime: 5.2.0 + libqp: 2.0.1 + dev: false + + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + semver: 6.3.1 + dev: false + optional: true + + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.6.0 + dev: true + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + dev: true + + /map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + dev: true + + /map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + dev: true + + /map-stream@0.0.7: + resolution: {integrity: sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==} + dev: true + + /marked@7.0.3: + resolution: {integrity: sha512-ev2uM40p0zQ/GbvqotfKcSWEa59fJwluGZj5dcaUOwDRrB1F3dncdXy8NWUApk4fi8atU3kTBOwjyjZ0ud0dxw==} + engines: {node: '>= 16'} + hasBin: true + dev: true + + /mathjs@12.4.0: + resolution: {integrity: sha512-4Moy0RNjwMSajEkGGxNUyMMC/CZAcl87WBopvNsJWB4E4EFebpTedr+0/rhqmnOSTH3Wu/3WfiWiw6mqiaHxVw==} + engines: {node: '>= 18'} + hasBin: true + dependencies: + '@babel/runtime': 7.23.9 + complex.js: 2.1.1 + decimal.js: 10.4.3 + escape-latex: 1.2.0 + fraction.js: 4.3.4 + javascript-natural-sort: 0.7.1 + seedrandom: 3.0.5 + tiny-emitter: 2.1.0 + typed-function: 4.1.1 + dev: false + + /memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + dependencies: + fs-monkey: 1.0.5 + dev: true + + /mensch@0.3.4: + resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==} + dev: false + + /meow@8.1.2: + resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} + engines: {node: '>=10'} + dependencies: + '@types/minimist': 1.2.5 + camelcase-keys: 6.2.2 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.18.1 + yargs-parser: 20.2.9 + dev: true + + /merge-source-map@1.0.4: + resolution: {integrity: sha512-PGSmS0kfnTnMJCzJ16BLLCEe6oeYCamKFFdQKshi4BmM6FUwipjVOcBFGxqtQtirtAG4iZvHlqST9CpZKqlRjA==} + dependencies: + source-map: 0.5.7 + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /merge@2.1.1: + resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==} + dev: true + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + + /mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + /mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + dev: false + + /mimic-fn@1.2.0: + resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} + engines: {node: '>=4'} + dev: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + + /min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + requiresBuild: true + dependencies: + brace-expansion: 2.0.1 + dev: false + + /minimatch@7.4.6: + resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@8.0.4: + resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: false + + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + + /minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + dev: true + + /minimist@1.2.7: + resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + /minio@7.1.3: + resolution: {integrity: sha512-xPrLjWkTT5E7H7VnzOjF//xBp9I40jYB4aWhb2xTFopXXfw+Wo82DDWngdUju7Doy3Wk7R8C4LAgwhLHHnf0wA==} + engines: {node: ^16 || ^18 || >=20} + dependencies: + async: 3.2.5 + block-stream2: 2.1.0 + browser-or-node: 2.1.1 + buffer-crc32: 0.2.13 + fast-xml-parser: 4.3.6 + ipaddr.js: 2.1.0 + json-stream: 1.0.0 + lodash: 4.17.21 + mime-types: 2.1.35 + query-string: 7.1.3 + through2: 4.0.2 + web-encoding: 1.1.5 + xml: 1.0.1 + xml2js: 0.5.0 + dev: false + + /minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + yallist: 4.0.0 + dev: false + optional: true + + /minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + dev: true + + /minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + requiresBuild: true + dev: false + optional: true + + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + + /minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + requiresBuild: true + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + dev: false + optional: true + + /mjml-accordion@4.14.1: + resolution: {integrity: sha512-dpNXyjnhYwhM75JSjD4wFUa9JgHm86M2pa0CoTzdv1zOQz67ilc4BoK5mc2S0gOjJpjBShM5eOJuCyVIuAPC6w==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-body@4.14.1: + resolution: {integrity: sha512-YpXcK3o2o1U+fhI8f60xahrhXuHmav6BZez9vIN3ZEJOxPFSr+qgr1cT2iyFz50L5+ZsLIVj2ZY+ALQjdsg8ig==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-button@4.14.1: + resolution: {integrity: sha512-V1Tl1vQ3lXYvvqHJHvGcc8URr7V1l/ZOsv7iLV4QRrh7kjKBXaRS7uUJtz6/PzEbNsGQCiNtXrODqcijLWlgaw==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-carousel@4.14.1: + resolution: {integrity: sha512-Ku3MUWPk/TwHxVgKEUtzspy/ePaWtN/3z6/qvNik0KIn0ZUIZ4zvR2JtaVL5nd30LHSmUaNj30XMPkCjYiKkFA==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-cli@4.14.1: + resolution: {integrity: sha512-Gy6MnSygFXs0U1qOXTHqBg2vZX2VL/fAacgQzD4MHq4OuybWaTNSzXRwxBXYCxT3IJB874n2Q0Mxp+Xka+tnZg==} + hasBin: true + dependencies: + '@babel/runtime': 7.23.9 + chokidar: 3.6.0 + glob: 7.2.3 + html-minifier: 4.0.0 + js-beautify: 1.14.11 + lodash: 4.17.21 + mjml-core: 4.14.1 + mjml-migrate: 4.14.1 + mjml-parser-xml: 4.14.1 + mjml-validator: 4.13.0 + yargs: 16.2.0 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-column@4.14.1: + resolution: {integrity: sha512-iixVCIX1YJtpQuwG2WbDr7FqofQrlTtGQ4+YAZXGiLThs0En3xNIJFQX9xJ8sgLEGGltyooHiNICBRlzSp9fDg==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-core@4.14.1: + resolution: {integrity: sha512-di88rSfX+8r4r+cEqlQCO7CRM4mYZrfe2wSCu2je38i+ujjkLpF72cgLnjBlSG5aOUCZgYvlsZ85stqIz9LQfA==} + dependencies: + '@babel/runtime': 7.23.9 + cheerio: 1.0.0-rc.12 + detect-node: 2.1.0 + html-minifier: 4.0.0 + js-beautify: 1.14.11 + juice: 9.1.0 + lodash: 4.17.21 + mjml-migrate: 4.14.1 + mjml-parser-xml: 4.14.1 + mjml-validator: 4.13.0 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-divider@4.14.1: + resolution: {integrity: sha512-agqWY0aW2xaMiUOhYKDvcAAfOLalpbbtjKZAl1vWmNkURaoK4L7MgDilKHSJDFUlHGm2ZOArTrq8i6K0iyThBQ==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-group@4.14.1: + resolution: {integrity: sha512-dJt5batgEJ7wxlxzqOfHOI94ABX+8DZBvAlHuddYO4CsLFHYv6XRIArLAMMnAKU76r6p3X8JxYeOjKZXdv49kg==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head-attributes@4.14.1: + resolution: {integrity: sha512-XdUNOp2csK28kBDSistInOyzWNwmu5HDNr4y1Z7vSQ1PfkmiuS6jWG7jHUjdoMhs27e6Leuyyc6a8gWSpqSWrg==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head-breakpoint@4.14.1: + resolution: {integrity: sha512-Qw9l/W/I5Z9p7I4ShgnEpAL9if4472ejcznbBnp+4Gq+sZoPa7iYoEPsa9UCGutlaCh3N3tIi2qKhl9qD8DFxA==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head-font@4.14.1: + resolution: {integrity: sha512-oBYm1gaOdEMjE5BoZouRRD4lCNZ1jcpz92NR/F7xDyMaKCGN6T/+r4S5dq1gOLm9zWqClRHaECdFJNEmrDpZqA==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head-html-attributes@4.14.1: + resolution: {integrity: sha512-vlJsJc1Sm4Ml2XvLmp01zsdmWmzm6+jNCO7X3eYi9ngEh8LjMCLIQOncnOgjqm9uGpQu2EgUhwvYFZP2luJOVg==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head-preview@4.14.1: + resolution: {integrity: sha512-89gQtt3fhl2dkYpHLF5HDQXz/RLpzecU6wmAIT7Dz6etjLGE1dgq2Ay6Bu/OeHjDcT1gbM131zvBwuXw8OydNw==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head-style@4.14.1: + resolution: {integrity: sha512-XryOuf32EDuUCBT2k99C1+H87IOM919oY6IqxKFJCDkmsbywKIum7ibhweJdcxiYGONKTC6xjuibGD3fQTTYNQ==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head-title@4.14.1: + resolution: {integrity: sha512-aIfpmlQdf1eJZSSrFodmlC4g5GudBti2eMyG42M7/3NeLM6anEWoe+UkF/6OG4Zy0tCQ40BDJ5iBZlMsjQICzw==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head@4.14.1: + resolution: {integrity: sha512-KoCbtSeTAhx05Ugn9TB2UYt5sQinSCb7RGRer5iPQ3CrXj8hT5B5Svn6qvf/GACPkWl4auExHQh+XgLB+r3OEA==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-hero@4.14.1: + resolution: {integrity: sha512-TQJ3yfjrKYGkdEWjHLHhL99u/meKFYgnfJvlo9xeBvRjSM696jIjdqaPHaunfw4CP6d2OpCIMuacgOsvqQMWOA==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-image@4.14.1: + resolution: {integrity: sha512-jfKLPHXuFq83okwlNM1Um/AEWeVDgs2JXIOsWp2TtvXosnRvGGMzA5stKLYdy1x6UfKF4c1ovpMS162aYGp+xQ==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-migrate@4.14.1: + resolution: {integrity: sha512-d+9HKQOhZi3ZFAaFSDdjzJX9eDQGjMf3BArLWNm2okC4ZgfJSpOc77kgCyFV8ugvwc8fFegPnSV60Jl4xtvK2A==} + hasBin: true + dependencies: + '@babel/runtime': 7.23.9 + js-beautify: 1.14.11 + lodash: 4.17.21 + mjml-core: 4.14.1 + mjml-parser-xml: 4.14.1 + yargs: 16.2.0 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-navbar@4.14.1: + resolution: {integrity: sha512-rNy1Kw8CR3WQ+M55PFBAUDz2VEOjz+sk06OFnsnmNjoMVCjo1EV7OFLDAkmxAwqkC8h4zQWEOFY0MBqqoAg7+A==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-parser-xml@4.14.1: + resolution: {integrity: sha512-9WQVeukbXfq9DUcZ8wOsHC6BTdhaVwTAJDYMIQglXLwKwN7I4pTCguDDHy5d0kbbzK5OCVxCdZe+bfVI6XANOQ==} + dependencies: + '@babel/runtime': 7.23.9 + detect-node: 2.0.4 + htmlparser2: 8.0.2 + lodash: 4.17.21 + dev: false + + /mjml-preset-core@4.14.1: + resolution: {integrity: sha512-uUCqK9Z9d39rwB/+JDV2KWSZGB46W7rPQpc9Xnw1DRP7wD7qAfJwK6AZFCwfTgWdSxw0PwquVNcrUS9yBa9uhw==} + dependencies: + '@babel/runtime': 7.23.9 + mjml-accordion: 4.14.1 + mjml-body: 4.14.1 + mjml-button: 4.14.1 + mjml-carousel: 4.14.1 + mjml-column: 4.14.1 + mjml-divider: 4.14.1 + mjml-group: 4.14.1 + mjml-head: 4.14.1 + mjml-head-attributes: 4.14.1 + mjml-head-breakpoint: 4.14.1 + mjml-head-font: 4.14.1 + mjml-head-html-attributes: 4.14.1 + mjml-head-preview: 4.14.1 + mjml-head-style: 4.14.1 + mjml-head-title: 4.14.1 + mjml-hero: 4.14.1 + mjml-image: 4.14.1 + mjml-navbar: 4.14.1 + mjml-raw: 4.14.1 + mjml-section: 4.14.1 + mjml-social: 4.14.1 + mjml-spacer: 4.14.1 + mjml-table: 4.14.1 + mjml-text: 4.14.1 + mjml-wrapper: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-raw@4.14.1: + resolution: {integrity: sha512-9+4wzoXnCtfV6QPmjfJkZ50hxFB4Z8QZnl2Ac0D1Cn3dUF46UkmO5NLMu7UDIlm5DdFyycZrMOwvZS4wv9ksPw==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-section@4.14.1: + resolution: {integrity: sha512-Ik5pTUhpT3DOfB3hEmAWp8rZ0ilWtIivnL8XdUJRfgYE9D+MCRn+reIO+DAoJHxiQoI6gyeKkIP4B9OrQ7cHQw==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-social@4.14.1: + resolution: {integrity: sha512-G44aOZXgZHukirjkeQWTTV36UywtE2YvSwWGNfo/8d+k5JdJJhCIrlwaahyKEAyH63G1B0Zt8b2lEWx0jigYUw==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-spacer@4.14.1: + resolution: {integrity: sha512-5SfQCXTd3JBgRH1pUy6NVZ0lXBiRqFJPVHBdtC3OFvUS3q1w16eaAXlIUWMKTfy8CKhQrCiE6m65kc662ZpYxA==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-table@4.14.1: + resolution: {integrity: sha512-aVBdX3WpyKVGh/PZNn2KgRem+PQhWlvnD00DKxDejRBsBSKYSwZ0t3EfFvZOoJ9DzfHsN0dHuwd6Z18Ps44NFQ==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-text@4.14.1: + resolution: {integrity: sha512-yZuvf5z6qUxEo5CqOhCUltJlR6oySKVcQNHwoV5sneMaKdmBiaU4VDnlYFera9gMD9o3KBHIX6kUg7EHnCwBRQ==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-validator@4.13.0: + resolution: {integrity: sha512-uURYfyQYtHJ6Qz/1A7/+E9ezfcoISoLZhYK3olsxKRViwaA2Mm8gy/J3yggZXnsUXWUns7Qymycm5LglLEIiQg==} + dependencies: + '@babel/runtime': 7.23.9 + dev: false + + /mjml-wrapper@4.14.1: + resolution: {integrity: sha512-aA5Xlq6d0hZ5LY+RvSaBqmVcLkvPvdhyAv3vQf3G41Gfhel4oIPmkLnVpHselWhV14A0KwIOIAKVxHtSAxyOTQ==} + dependencies: + '@babel/runtime': 7.23.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + mjml-section: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml@4.14.1: + resolution: {integrity: sha512-f/wnWWIVbeb/ge3ff7c/KYYizI13QbGIp03odwwkCThsJsacw4gpZZAU7V4gXY3HxSXP2/q3jxOfaHVbkfNpOQ==} + hasBin: true + dependencies: + '@babel/runtime': 7.23.9 + mjml-cli: 4.14.1 + mjml-core: 4.14.1 + mjml-migrate: 4.14.1 + mjml-preset-core: 4.14.1 + mjml-validator: 4.13.0 + transitivePeerDependencies: + - encoding + dev: false + + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + requiresBuild: true + dev: false + optional: true + + /mkdirp@2.1.6: + resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} + engines: {node: '>=10'} + hasBin: true + + /mnemonist@0.39.6: + resolution: {integrity: sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==} + dependencies: + obliterator: 2.0.4 + dev: false + + /mockdate@3.0.5: + resolution: {integrity: sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==} + dev: false + + /modify-values@1.0.1: + resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} + engines: {node: '>=0.10.0'} + dev: true + + /moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + /morgan@1.10.0: + resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} + engines: {node: '>= 0.8.0'} + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + /msgpackr-extract@3.0.2: + resolution: {integrity: sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==} + hasBin: true + requiresBuild: true + dependencies: + node-gyp-build-optional-packages: 5.0.7 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.2 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.2 + dev: false + optional: true + + /msgpackr@1.10.1: + resolution: {integrity: sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==} + optionalDependencies: + msgpackr-extract: 3.0.2 + dev: false + + /mute-stream@0.0.7: + resolution: {integrity: sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==} + dev: true + + /mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + dev: true + + /mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + + /mysql2@3.9.1: + resolution: {integrity: sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==} + engines: {node: '>= 8.0'} + dependencies: + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.6.3 + long: 5.2.3 + lru-cache: 8.0.5 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + dev: false + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: false + + /named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + dependencies: + lru-cache: 7.18.3 + dev: false + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: false + + /natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + /nestjs-minio@2.5.4(@nestjs/common@10.3.3)(@nestjs/core@10.3.3): + resolution: {integrity: sha512-b99fCEjK1Kt7cNDfANrfhckQiYC10mKoVqfdwUJQaVCWMKTm2E8Kb7oiPIyWyjcVP6RERn5oUMPmf/rZLJEr3A==} + peerDependencies: + '@nestjs/common': '>7.0.0' + '@nestjs/core': '>7.0.0' + dependencies: + '@nestjs/common': 10.3.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.3(@nestjs/common@10.3.3)(@nestjs/websockets@10.3.3)(reflect-metadata@0.2.1)(rxjs@7.8.1) + minio: 7.1.3 + dev: false + + /next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + dev: true + + /nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + dev: false + + /no-case@2.3.2: + resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} + dependencies: + lower-case: 1.1.4 + dev: false + + /node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + dev: true + + /node-addon-api@3.2.1: + resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} + requiresBuild: true + dev: false + optional: true + + /node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + dependencies: + lodash: 4.17.21 + dev: true + + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + + /node-gyp-build-optional-packages@5.0.7: + resolution: {integrity: sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==} + hasBin: true + requiresBuild: true + dev: false + optional: true + + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: true + + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + dev: true + + /nodejieba@2.5.2: + resolution: {integrity: sha512-ByskJvaBrQ2eV+5M0OeD80S5NKoGaHc9zi3Z/PTKl/95eac2YF8RmWduq9AknLpkQLrLAIcqurrtC6BzjpKwwg==} + engines: {node: '>= 10.20.0'} + requiresBuild: true + dependencies: + '@mapbox/node-pre-gyp': 1.0.11 + node-addon-api: 3.2.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + optional: true + + /nodemailer@6.9.9: + resolution: {integrity: sha512-dexTll8zqQoVJEZPwQAKzxxtFn0qTnjdQTchoU6Re9BUUGBJiOy3YMn/0ShTW6J5M0dfQ1NeDeRTTl4oIWgQMA==} + engines: {node: '>=6.0.0'} + dev: false + + /nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + requiresBuild: true + dependencies: + abbrev: 1.1.1 + dev: false + optional: true + + /nopt@7.2.0: + resolution: {integrity: sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + dependencies: + abbrev: 2.0.0 + dev: false + + /normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.8 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.13.1 + semver: 7.6.0 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + /notepack.io@3.0.1: + resolution: {integrity: sha512-TKC/8zH5pXIAMVQio2TvVDTtPRX+DJPHDqjRbxogtFiByHyzKmy96RA0JtCQJ+WouyyL4A10xomQzgbUT+1jCg==} + dev: false + + /npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + dependencies: + path-key: 2.0.1 + dev: false + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: true + + /npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + requiresBuild: true + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + dev: false + optional: true + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + + /oauth@0.10.0: + resolution: {integrity: sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==} + dev: false + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + + /object-is@1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /obliterator@2.0.4: + resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} + dev: false + + /on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + dev: false + + /on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: true + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: true + + /on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + + /one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + dependencies: + fn.name: 1.1.0 + dev: false + + /onetime@2.0.1: + resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} + engines: {node: '>=4'} + dependencies: + mimic-fn: 1.2.0 + dev: true + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + + /open@7.4.2: + resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} + engines: {node: '>=8'} + dependencies: + is-docker: 2.2.1 + is-wsl: 2.2.0 + dev: false + + /open@8.4.0: + resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} + engines: {node: '>=12'} + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + dev: true + + /opencollective-postinstall@2.0.3: + resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} + hasBin: true + dev: true + + /opentype.js@0.7.3: + resolution: {integrity: sha512-Veui5vl2bLonFJ/SjX/WRWJT3SncgiZNnKUyahmXCc2sa1xXW15u3R/3TN5+JFiP7RsjK5ER4HA5eWaEmV9deA==} + hasBin: true + dependencies: + tiny-inflate: 1.0.3 + dev: false + + /optionator@0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.5 + dev: true + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + dev: true + + /os-homedir@1.0.2: + resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} + engines: {node: '>=0.10.0'} + dev: true + + /os-name@1.0.3: + resolution: {integrity: sha512-f5estLO2KN8vgtTRaILIgEGBoBrMnZ3JQ7W9TMZCnOIGwHe8TRGSpcagnWDo+Dfhd/z08k9Xe75hvciJJ8Qaew==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + osx-release: 1.1.0 + win-release: 1.1.1 + dev: false + + /os-name@4.0.1: + resolution: {integrity: sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw==} + engines: {node: '>=10'} + dependencies: + macos-release: 2.5.1 + windows-release: 4.0.0 + dev: true + + /os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + dev: true + + /osx-release@1.1.0: + resolution: {integrity: sha512-ixCMMwnVxyHFQLQnINhmIpWqXIfS2YOXchwQrk+OFzmo6nDjQ0E4KXAyyUh0T0MZgV4bUhkRrAbVqlE4yLVq4A==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: false + + /p-event@4.2.0: + resolution: {integrity: sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==} + engines: {node: '>=8'} + dependencies: + p-timeout: 3.2.0 + dev: false + + /p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + dev: false + + /p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + dependencies: + p-try: 1.0.0 + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + dependencies: + p-limit: 1.3.0 + dev: true + + /p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + dependencies: + p-finally: 1.0.0 + dev: false + + /p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + + /p-wait-for@3.2.0: + resolution: {integrity: sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==} + engines: {node: '>=8'} + dependencies: + p-timeout: 3.2.0 + dev: false + + /pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + dev: true + + /pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + dev: false + + /param-case@2.1.1: + resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} + dependencies: + no-case: 2.3.2 + dev: false + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + + /parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + dependencies: + error-ex: 1.3.2 + json-parse-better-errors: 1.0.2 + dev: true + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.23.5 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + /parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + dev: true + + /parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + dependencies: + parse5: 6.0.1 + dev: false + + /parse5-htmlparser2-tree-adapter@7.0.0: + resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + dependencies: + domhandler: 5.0.3 + parse5: 7.1.2 + + /parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + dev: false + + /parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + dev: false + + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + + /parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + dev: false + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: true + + /passport-google-oauth20@2.0.0: + resolution: {integrity: sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==} + engines: {node: '>= 0.4.0'} + dependencies: + passport-oauth2: 1.8.0 + dev: false + + /passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + dependencies: + jsonwebtoken: 9.0.2 + passport-strategy: 1.0.0 + dev: false + + /passport-local@1.0.0: + resolution: {integrity: sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==} + engines: {node: '>= 0.4.0'} + dependencies: + passport-strategy: 1.0.0 + dev: false + + /passport-oauth2@1.8.0: + resolution: {integrity: sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==} + engines: {node: '>= 0.4.0'} + dependencies: + base64url: 3.0.1 + oauth: 0.10.0 + passport-strategy: 1.0.0 + uid2: 0.0.4 + utils-merge: 1.0.1 + dev: false + + /passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + dev: false + + /passport@0.7.0: + resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==} + engines: {node: '>= 0.4.0'} + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + dev: false + + /path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + dev: true + + /path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + /path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + dev: false + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.2.0 + minipass: 7.0.4 + + /path-to-regexp@3.2.0: + resolution: {integrity: sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==} + + /path-to-regexp@6.2.1: + resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} + dev: false + + /path-type@3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + dependencies: + pify: 3.0.0 + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + /pause-stream@0.0.11: + resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} + dependencies: + through: 2.3.8 + + /pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + dev: false + + /pdfjs-dist@2.12.313: + resolution: {integrity: sha512-1x6iXO4Qnv6Eb+YFdN5JdUzt4pAkxSp3aLAYPX93eQCyg/m7QFzXVWJHJVtoW48CI8HCXju4dSkhQZwoheL5mA==} + peerDependencies: + worker-loader: ^3.0.8 + peerDependenciesMeta: + worker-loader: + optional: true + dev: true + + /pdfmake@0.2.9: + resolution: {integrity: sha512-LAtYwlR8cCQqbxESK2d50DYaVAzAC9Id9NjilRte6Tb9pyHUB+Z50nhD0imuBL0eDyXQKvEYSNjo3P5AOc2ZCg==} + engines: {node: '>=12'} + dependencies: + '@foliojs-fork/linebreak': 1.1.1 + '@foliojs-fork/pdfkit': 0.14.0 + iconv-lite: 0.6.3 + xmldoc: 1.3.0 + dev: true + + /peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + dev: false + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + /picomatch@3.0.1: + resolution: {integrity: sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==} + engines: {node: '>=10'} + dev: true + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + dev: true + + /pino-abstract-transport@1.1.0: + resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==} + dependencies: + readable-stream: 4.5.2 + split2: 4.2.0 + dev: false + + /pino-std-serializers@6.2.2: + resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} + dev: false + + /pino@8.18.0: + resolution: {integrity: sha512-Mz/gKiRyuXu4HnpHgi1YWdHQCoWMufapzooisvFn78zl4dZciAxS+YeRkUxXl1ee/SzU80YCz1zpECCh4oC6Aw==} + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.3.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.1.0 + pino-std-serializers: 6.2.2 + process-warning: 3.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.3 + sonic-boom: 3.8.0 + thread-stream: 2.4.1 + dev: false + + /pinyin@3.1.0: + resolution: {integrity: sha512-U+COtcFr2eRztdE9is+2EQCrrkTiSncizW/d58lhzINvjhCAWUOoIsaEL1DDX8GZrT5FoW69fi2dtWHjQlk/fw==} + engines: {install-node: ^18.0.0} + hasBin: true + dependencies: + commander: 1.1.1 + optionalDependencies: + '@node-rs/jieba': 1.10.0 + nodejieba: 2.5.2 + segmentit: 2.0.3 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + dev: true + + /pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + dev: true + + /png-js@1.0.0: + resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==} + dev: true + + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: false + + /prelude-ls@1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + dev: true + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + dependencies: + fast-diff: 1.3.0 + dev: true + + /prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + + /preval.macro@4.0.0: + resolution: {integrity: sha512-sJJnE71X+MPr64CVD2AurmUj4JEDqbudYbStav3L9Xjcqm4AR0ymMm6sugw1mUmfI/7gw4JWA4JXo/k6w34crw==} + requiresBuild: true + dependencies: + babel-plugin-preval: 4.0.0 + dev: false + optional: true + + /preview-email@3.0.19: + resolution: {integrity: sha512-DBS3Nir18YtKc8loYCCOGitmiaQ0vTdahPoiXxwNweJDpmVZo+w3tppufOhoK0m8skpRxT56llYLs3VrORnmNQ==} + engines: {node: '>=14'} + dependencies: + ci-info: 3.9.0 + display-notification: 2.0.0 + fixpack: 4.0.0 + get-port: 5.1.1 + mailparser: 3.6.7 + nodemailer: 6.9.9 + open: 7.4.2 + p-event: 4.2.0 + p-wait-for: 3.2.0 + pug: 3.0.2 + uuid: 9.0.1 + dev: false + + /prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + dev: true + + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + /process-warning@2.3.2: + resolution: {integrity: sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==} + dev: false + + /process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + dev: false + + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: false + + /promise-coalesce@1.1.2: + resolution: {integrity: sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==} + engines: {node: '>=16'} + dev: false + + /promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + requiresBuild: true + dependencies: + asap: 2.0.6 + dev: false + + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: true + + /propagating-hammerjs@1.5.0: + resolution: {integrity: sha512-3PUXWmomwutoZfydC+lJwK1bKCh6sK6jZGB31RUX6+4EXzsbkDZrK4/sVR7gBrvJaEIwpTVyxQUAd29FKkmVdw==} + dependencies: + hammerjs: 2.0.8 + dev: true + + /proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + dev: false + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + + /proxy-middleware@0.15.0: + resolution: {integrity: sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q==} + engines: {node: '>=0.8.0'} + dev: true + + /pug-attrs@3.0.0: + resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==} + requiresBuild: true + dependencies: + constantinople: 4.0.1 + js-stringify: 1.0.2 + pug-runtime: 3.0.1 + dev: false + + /pug-code-gen@3.0.2: + resolution: {integrity: sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==} + requiresBuild: true + dependencies: + constantinople: 4.0.1 + doctypes: 1.1.0 + js-stringify: 1.0.2 + pug-attrs: 3.0.0 + pug-error: 2.0.0 + pug-runtime: 3.0.1 + void-elements: 3.1.0 + with: 7.0.2 + dev: false + + /pug-error@2.0.0: + resolution: {integrity: sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==} + requiresBuild: true + dev: false + + /pug-filters@4.0.0: + resolution: {integrity: sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==} + requiresBuild: true + dependencies: + constantinople: 4.0.1 + jstransformer: 1.0.0 + pug-error: 2.0.0 + pug-walk: 2.0.0 + resolve: 1.22.8 + dev: false + + /pug-lexer@5.0.1: + resolution: {integrity: sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==} + requiresBuild: true + dependencies: + character-parser: 2.2.0 + is-expression: 4.0.0 + pug-error: 2.0.0 + dev: false + + /pug-linker@4.0.0: + resolution: {integrity: sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==} + requiresBuild: true + dependencies: + pug-error: 2.0.0 + pug-walk: 2.0.0 + dev: false + + /pug-load@3.0.0: + resolution: {integrity: sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==} + requiresBuild: true + dependencies: + object-assign: 4.1.1 + pug-walk: 2.0.0 + dev: false + + /pug-parser@6.0.0: + resolution: {integrity: sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==} + requiresBuild: true + dependencies: + pug-error: 2.0.0 + token-stream: 1.0.0 + dev: false + + /pug-runtime@3.0.1: + resolution: {integrity: sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==} + requiresBuild: true + dev: false + + /pug-strip-comments@2.0.0: + resolution: {integrity: sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==} + requiresBuild: true + dependencies: + pug-error: 2.0.0 + dev: false + + /pug-walk@2.0.0: + resolution: {integrity: sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==} + requiresBuild: true + dev: false + + /pug@3.0.2: + resolution: {integrity: sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==} + dependencies: + pug-code-gen: 3.0.2 + pug-filters: 4.0.0 + pug-lexer: 5.0.1 + pug-linker: 4.0.0 + pug-load: 3.0.0 + pug-parser: 6.0.0 + pug-runtime: 3.0.1 + pug-strip-comments: 2.0.0 + dev: false + + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + /pure-rand@6.0.4: + resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} + dev: true + + /q@1.5.1: + resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} + engines: {node: '>=0.6.0', teleport: '>=0.2.0'} + dev: true + + /qiniu@7.11.0: + resolution: {integrity: sha512-Pdux9AxQR5V8IrlkSWDBUIrBRoxyK98sfmdGm19R0jZyxBMM2+KMwB0zhjAJhb6+lxEzjyHO3EfsVRz0JeTj7A==} + engines: {node: '>= 6'} + dependencies: + agentkeepalive: 4.5.0 + before: 0.0.1 + block-stream2: 2.1.0 + crc32: 0.2.2 + destroy: 1.2.0 + encodeurl: 1.0.2 + formstream: 1.3.1 + mime: 2.6.0 + mockdate: 3.0.5 + tunnel-agent: 0.6.0 + urllib: 2.41.0 + transitivePeerDependencies: + - proxy-agent + - supports-color + dev: false + + /qs@6.11.2: + resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.5 + + /query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + dev: false + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + dev: false + + /quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + dev: true + + /quote-stream@1.0.2: + resolution: {integrity: sha512-kKr2uQ2AokadPjvTyKJQad9xELbZwYzWlNfI3Uz2j/ib5u6H9lDP7fUUR//rMycd0gv4Z5P1qXMfXR8YpIxrjQ==} + hasBin: true + dependencies: + buffer-equal: 0.0.1 + minimist: 1.2.8 + through2: 2.0.5 + dev: true + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: true + + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: false + + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: true + + /read-pkg-up@3.0.0: + resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} + engines: {node: '>=4'} + dependencies: + find-up: 2.1.0 + read-pkg: 3.0.0 + dev: true + + /read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + dev: true + + /read-pkg@3.0.0: + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} + engines: {node: '>=4'} + dependencies: + load-json-file: 4.0.0 + normalize-package-data: 2.5.0 + path-type: 3.0.0 + dev: true + + /read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + dev: true + + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + /readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + dev: false + + /readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + dependencies: + minimatch: 5.1.6 + dev: false + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + + /real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + dev: false + + /rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + dependencies: + resolve: 1.22.8 + dev: true + + /redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: true + + /redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + dev: false + + /redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + dependencies: + redis-errors: 1.2.0 + dev: false + + /reflect-metadata@0.1.14: + resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} + dev: false + + /reflect-metadata@0.2.1: + resolution: {integrity: sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==} + + /regenerate-unicode-properties@10.1.1: + resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + dev: true + + /regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + dev: true + + /regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + /regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + dependencies: + '@babel/runtime': 7.23.9 + dev: true + + /regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.1 + dev: true + + /regexpu-core@5.3.2: + resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} + engines: {node: '>=4'} + dependencies: + '@babel/regjsgen': 0.8.0 + regenerate: 1.4.2 + regenerate-unicode-properties: 10.1.1 + regjsparser: 0.9.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.1.0 + dev: true + + /regjsparser@0.9.1: + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + hasBin: true + dependencies: + jsesc: 0.5.0 + dev: true + + /relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + dev: false + + /repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: true + + /resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + + /resolve-global@1.0.0: + resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + global-dirs: 0.1.1 + dev: true + optional: true + + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + dev: true + + /resolve@1.1.7: + resolution: {integrity: sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==} + dev: true + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + /restore-cursor@2.0.0: + resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} + engines: {node: '>=4'} + dependencies: + onetime: 2.0.1 + signal-exit: 3.0.7 + dev: true + + /restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: true + + /ret@0.2.2: + resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} + engines: {node: '>=4'} + dev: false + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + /rfdc@1.3.1: + resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} + dev: false + + /rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + hasBin: true + dependencies: + glob: 7.2.3 + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + + /rimraf@4.4.1: + resolution: {integrity: sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 9.3.5 + dev: true + + /rimraf@5.0.5: + resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 10.3.10 + dev: false + + /run-applescript@3.2.0: + resolution: {integrity: sha512-Ep0RsvAjnRcBX1p5vogbaBdAGu/8j/ewpvGqnQYunnLd9SM0vWcPJewPKNnWFggf0hF0pwIgwV5XK7qQ7UZ8Qg==} + engines: {node: '>=4'} + dependencies: + execa: 0.10.0 + dev: false + + /run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + dev: true + + /run-async@3.0.0: + resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} + engines: {node: '>=0.12.0'} + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + dependencies: + tslib: 1.14.1 + dev: true + + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + dependencies: + tslib: 2.6.2 + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + /safe-regex2@2.0.0: + resolution: {integrity: sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==} + dependencies: + ret: 0.2.2 + dev: false + + /safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + /sax@1.3.0: + resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} + + /saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} + dependencies: + xmlchars: 2.2.0 + dev: false + + /schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: true + + /scope-analyzer@2.1.2: + resolution: {integrity: sha512-5cfCmsTYV/wPaRIItNxatw02ua/MThdIUNnUOCYp+3LSEJvnG804ANw2VLaavNILIfWXF1D1G2KNANkBBvInwQ==} + dependencies: + array-from: 2.1.1 + dash-ast: 2.0.1 + es6-map: 0.1.5 + es6-set: 0.1.6 + es6-symbol: 3.1.3 + estree-is-function: 1.0.0 + get-assigned-identifiers: 1.2.0 + dev: true + + /secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + dev: false + + /seedrandom@3.0.5: + resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} + dev: false + + /segmentit@2.0.3: + resolution: {integrity: sha512-7mn2XL3OdTUQ+AhHz7SbgyxLTaQRzTWQNVwiK+UlTO8aePGbSwvKUzTwE4238+OUY9MoR6ksAg35zl8sfTunQQ==} + requiresBuild: true + dependencies: + preval.macro: 4.0.0 + dev: false + optional: true + + /selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + dependencies: + parseley: 0.12.1 + dev: false + + /semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + /semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + + /send@1.0.0-beta.2: + resolution: {integrity: sha512-k1yHu/FNK745PULKdsGpQ+bVSXYNwSk+bWnYzbxGZbt5obZc0JKDVANsCRuJD1X/EG15JtP9eZpwxkhUxIYEcg==} + engines: {node: '>= 0.10'} + dependencies: + debug: 3.1.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime-types: 2.1.35 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + dev: false + + /serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + dependencies: + randombytes: 2.1.0 + dev: true + + /serve-index@1.9.1: + resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} + engines: {node: '>= 0.8.0'} + dependencies: + accepts: 1.3.8 + batch: 0.6.1 + debug: 2.6.9 + escape-html: 1.0.3 + http-errors: 1.6.3 + mime-types: 2.1.35 + parseurl: 1.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + requiresBuild: true + dev: false + optional: true + + /set-cookie-parser@2.6.0: + resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} + dev: false + + /set-function-length@1.2.1: + resolution: {integrity: sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + dev: true + + /setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + dev: false + + /setprototypeof@1.1.0: + resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + dev: true + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + /sha.js@2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: false + + /shallow-copy@0.0.1: + resolution: {integrity: sha512-b6i4ZpVuUxB9h5gfCxPiusKYkqTMOjEbBs4wMaFbkfia4yFv92UKZ6Df8WXcKbn08JNL/abvg3FnMAOfakDvUw==} + dev: true + + /shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + dependencies: + shebang-regex: 1.0.0 + dev: false + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + + /shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + dev: false + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + /shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + dev: true + + /side-channel@1.0.5: + resolution: {integrity: sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.1 + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + /simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + dependencies: + is-arrayish: 0.3.2 + dev: false + + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /slick@1.12.2: + resolution: {integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==} + dev: false + + /socket.io-adapter@2.5.2: + resolution: {integrity: sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==} + dependencies: + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + /socket.io@4.7.4: + resolution: {integrity: sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==} + engines: {node: '>=10.2.0'} + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.4 + engine.io: 6.5.4 + socket.io-adapter: 2.5.2 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + /sonic-boom@3.8.0: + resolution: {integrity: sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==} + dependencies: + atomic-sleep: 1.0.0 + dev: false + + /source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.1.43: + resolution: {integrity: sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==} + engines: {node: '>=0.8.0'} + requiresBuild: true + dependencies: + amdefine: 1.0.1 + dev: true + optional: true + + /source-map@0.5.6: + resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==} + engines: {node: '>=0.10.0'} + dev: false + + /source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + /source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + dev: true + + /sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + dev: true + + /spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.17 + dev: true + + /spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + dev: true + + /spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.17 + dev: true + + /spdx-license-ids@3.0.17: + resolution: {integrity: sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==} + dev: true + + /split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + dev: false + + /split2@3.2.2: + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + dependencies: + readable-stream: 3.6.2 + dev: true + + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + dev: false + + /split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + dependencies: + through: 2.3.8 + dev: true + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + + /sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + dev: false + + /stack-generator@2.0.10: + resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + dependencies: + stackframe: 1.3.4 + dev: false + + /stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + dev: false + + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + dev: true + + /stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + dev: false + + /stacktrace-gps@3.1.2: + resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==} + dependencies: + source-map: 0.5.6 + stackframe: 1.3.4 + dev: false + + /stacktrace-js@2.0.2: + resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + dependencies: + error-stack-parser: 2.1.4 + stack-generator: 2.0.10 + stacktrace-gps: 3.1.2 + dev: false + + /standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + dev: false + + /standard-version@9.5.0: + resolution: {integrity: sha512-3zWJ/mmZQsOaO+fOlsa0+QK90pwhNd042qEcw6hKFNoLFs7peGyvPffpEBbK/DSGPbyOvli0mUIFv5A4qTjh2Q==} + engines: {node: '>=10'} + hasBin: true + dependencies: + chalk: 2.4.2 + conventional-changelog: 3.1.25 + conventional-changelog-config-spec: 2.1.0 + conventional-changelog-conventionalcommits: 4.6.3 + conventional-recommended-bump: 6.1.0 + detect-indent: 6.1.0 + detect-newline: 3.1.0 + dotgitignore: 2.1.0 + figures: 3.2.0 + find-up: 5.0.0 + git-semver-tags: 4.1.1 + semver: 7.6.0 + stringify-package: 1.0.1 + yargs: 16.2.0 + dev: true + + /static-eval@2.1.1: + resolution: {integrity: sha512-MgWpQ/ZjGieSVB3eOJVs4OA2LT/q1vx98KPCTTQPzq/aLr0YUXTsgryTXr4SLfR0ZfUUCiedM9n/ABeDIyy4mA==} + dependencies: + escodegen: 2.1.0 + dev: true + + /static-module@3.0.4: + resolution: {integrity: sha512-gb0v0rrgpBkifXCa3yZXxqVmXDVE+ETXj6YlC/jt5VzOnGXR2C15+++eXuMDUYsePnbhf+lwW0pE1UXyOLtGCw==} + dependencies: + acorn-node: 1.8.2 + concat-stream: 1.6.2 + convert-source-map: 1.9.0 + duplexer2: 0.1.4 + escodegen: 1.14.3 + has: 1.0.4 + magic-string: 0.25.1 + merge-source-map: 1.0.4 + object-inspect: 1.13.1 + readable-stream: 2.3.8 + scope-analyzer: 2.1.2 + shallow-copy: 0.0.1 + static-eval: 2.1.1 + through2: 2.0.5 + dev: true + + /statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + /stream-combiner@0.2.2: + resolution: {integrity: sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==} + dependencies: + duplexer: 0.1.2 + through: 2.3.8 + dev: true + + /stream-wormhole@1.1.0: + resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==} + engines: {node: '>=4.0.0'} + dev: false + + /strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + dev: false + + /string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + dev: true + + /string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} + dependencies: + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + + /stringify-package@1.0.1: + resolution: {integrity: sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==} + deprecated: This module is not used anymore, and has been replaced by @npmcli/package-json + dev: true + + /strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + dependencies: + ansi-regex: 3.0.1 + dev: true + + /strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + dependencies: + ansi-regex: 4.1.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + dev: true + + /strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + dev: false + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: true + + /strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: false + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + dev: false + + /superagent@8.1.2: + resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} + engines: {node: '>=6.4.0 <13 || >=14'} + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.3.4 + fast-safe-stringify: 2.1.1 + form-data: 4.0.0 + formidable: 2.1.2 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.11.2 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + dev: true + + /supertest@6.3.4: + resolution: {integrity: sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==} + engines: {node: '>=6.4.0'} + dependencies: + methods: 1.1.2 + superagent: 8.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + /svg-captcha@1.4.0: + resolution: {integrity: sha512-/fkkhavXPE57zRRCjNqAP3txRCSncpMx3NnNZL7iEoyAtYwUjPhJxW6FQTQPG5UPEmCrbFoXS10C3YdJlW7PDg==} + engines: {node: '>=4.x'} + dependencies: + opentype.js: 0.7.3 + dev: false + + /svg-pan-zoom@3.6.1: + resolution: {integrity: sha512-JaKkGHHfGvRrcMPdJWkssLBeWqM+Isg/a09H7kgNNajT1cX5AztDTNs+C8UzpCxjCTRrG34WbquwaovZbmSk9g==} + dev: true + + /swagger-ui-dist@5.11.2: + resolution: {integrity: sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A==} + dev: false + + /symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + dev: true + + /systeminformation@5.22.0: + resolution: {integrity: sha512-oAP80ymt8ssrAzjX8k3frbL7ys6AotqC35oikG6/SG15wBw+tG9nCk4oPaXIhEaAOAZ8XngxUv3ORq2IuR3r4Q==} + engines: {node: '>=8.0.0'} + os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] + hasBin: true + dev: false + + /tablesort@5.3.0: + resolution: {integrity: sha512-WkfcZBHsp47gVH9CBHG0ZXopriG01IA87arGrchvIe868d4RiXVvoYPS1zMq9IdW05kBs5iGsqxTABqLyWonbg==} + dev: true + + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + dev: true + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + + /tar@6.2.0: + resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==} + engines: {node: '>=10'} + requiresBuild: true + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: false + optional: true + + /temp@0.9.4: + resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} + engines: {node: '>=6.0.0'} + dependencies: + mkdirp: 0.5.6 + rimraf: 2.6.3 + dev: true + + /terser-webpack-plugin@5.3.10(webpack@5.90.1): + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.22 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.27.1 + webpack: 5.90.1 + dev: true + + /terser@5.27.1: + resolution: {integrity: sha512-29wAr6UU/oQpnTw5HoadwjUZnFQXGdOfj0LjZ4sVxzqwHh/QVkvr7m8y9WoR4iN3FRitVduTc6KdjcW38Npsug==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.5 + acorn: 8.11.3 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: true + + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + + /text-decoding@1.0.0: + resolution: {integrity: sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==} + dev: false + + /text-extensions@1.9.0: + resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} + engines: {node: '>=0.10'} + dev: true + + /text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + dev: false + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: false + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: false + + /thread-stream@2.4.1: + resolution: {integrity: sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==} + dependencies: + real-require: 0.2.0 + dev: false + + /through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + dev: true + + /through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + dependencies: + readable-stream: 3.6.2 + + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + /tiny-emitter@2.1.0: + resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==} + dev: false + + /tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + /tlds@1.248.0: + resolution: {integrity: sha512-noj0KdpWTBhwsKxMOXk0rN9otg4kTgLm4WohERRHbJ9IY+kSDKr3RmjitaQ3JFzny+DyvBOQKlFZhp0G0qNSfg==} + hasBin: true + dev: false + + /tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + dependencies: + os-tmpdir: 1.0.2 + dev: true + + /tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + dev: false + + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + + /toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + dev: false + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + /token-stream@1.0.0: + resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} + requiresBuild: true + dev: false + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + /traverse@0.3.9: + resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} + dev: false + + /traverse@0.6.8: + resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==} + engines: {node: '>= 0.4'} + dev: true + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + + /trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + dev: true + + /triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + dev: false + + /ts-jest@29.1.2(@babel/core@7.23.9)(jest@29.7.0)(typescript@5.3.3): + resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==} + engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.23.9 + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@20.11.18)(ts-node@10.9.2) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.6.0 + typescript: 5.3.3 + yargs-parser: 21.1.1 + dev: true + + /ts-loader@9.5.1(typescript@5.3.3)(webpack@5.90.1): + resolution: {integrity: sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==} + engines: {node: '>=12.0.0'} + peerDependencies: + typescript: '*' + webpack: ^5.0.0 + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.15.0 + micromatch: 4.0.5 + semver: 7.6.0 + source-map: 0.7.4 + typescript: 5.3.3 + webpack: 5.90.1 + dev: true + + /ts-morph@20.0.0: + resolution: {integrity: sha512-JVmEJy2Wow5n/84I3igthL9sudQ8qzjh/6i4tmYCm6IqYyKFlNbJZi7oBdjyqcWSWYRu3CtL0xbT6fS03ESZIg==} + dependencies: + '@ts-morph/common': 0.21.0 + code-block-writer: 12.0.0 + dev: true + + /ts-node@10.9.2(@types/node@20.11.18)(typescript@5.3.3): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.11.18 + acorn: 8.11.3 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.3.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + /tsconfig-paths-webpack-plugin@4.1.0: + resolution: {integrity: sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==} + engines: {node: '>=10.13.0'} + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.15.0 + tsconfig-paths: 4.2.0 + dev: true + + /tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + + /tslib@2.4.1: + resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} + dev: false + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + + /tsutils@3.21.0(typescript@5.3.3): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 5.3.3 + dev: true + + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /type-check@0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + + /type-fest@0.18.1: + resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + dev: true + + /type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + dev: true + + /type@1.2.0: + resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==} + dev: true + + /type@2.7.2: + resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==} + dev: true + + /typed-function@4.1.1: + resolution: {integrity: sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ==} + engines: {node: '>= 14'} + dev: false + + /typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: true + + /typeorm@0.3.17(ioredis@5.3.2)(mysql2@3.9.1)(ts-node@10.9.2): + resolution: {integrity: sha512-UDjUEwIQalO9tWw9O2A4GU+sT3oyoUXheHJy4ft+RFdnRdQctdQ34L9SqE2p7LdwzafHx1maxT+bqXON+Qnmig==} + engines: {node: '>= 12.9.0'} + hasBin: true + peerDependencies: + '@google-cloud/spanner': ^5.18.0 + '@sap/hana-client': ^2.12.25 + better-sqlite3: ^7.1.2 || ^8.0.0 + hdb-pool: ^0.1.6 + ioredis: ^5.0.4 + mongodb: ^5.2.0 + mssql: ^9.1.1 + mysql2: ^2.2.5 || ^3.0.1 + oracledb: ^5.1.0 + pg: ^8.5.1 + pg-native: ^3.0.0 + pg-query-stream: ^4.0.0 + redis: ^3.1.1 || ^4.0.0 + sql.js: ^1.4.0 + sqlite3: ^5.0.3 + ts-node: ^10.7.0 + typeorm-aurora-data-api-driver: ^2.0.0 + peerDependenciesMeta: + '@google-cloud/spanner': + optional: true + '@sap/hana-client': + optional: true + better-sqlite3: + optional: true + hdb-pool: + optional: true + ioredis: + optional: true + mongodb: + optional: true + mssql: + optional: true + mysql2: + optional: true + oracledb: + optional: true + pg: + optional: true + pg-native: + optional: true + pg-query-stream: + optional: true + redis: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + ts-node: + optional: true + typeorm-aurora-data-api-driver: + optional: true + dependencies: + '@sqltools/formatter': 1.2.5 + app-root-path: 3.1.0 + buffer: 6.0.3 + chalk: 4.1.2 + cli-highlight: 2.1.11 + date-fns: 2.30.0 + debug: 4.3.4 + dotenv: 16.4.4 + glob: 8.1.0 + ioredis: 5.3.2 + mkdirp: 2.1.6 + mysql2: 3.9.1 + reflect-metadata: 0.1.14 + sha.js: 2.4.11 + ts-node: 10.9.2(@types/node@20.11.18)(typescript@5.3.3) + tslib: 2.6.2 + uuid: 9.0.1 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + dev: false + + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + + /ua-parser-js@1.0.37: + resolution: {integrity: sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==} + dev: false + + /uc.micro@2.0.0: + resolution: {integrity: sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==} + dev: false + + /uglify-js@3.17.4: + resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + engines: {node: '>=0.8.0'} + hasBin: true + + /uid2@0.0.4: + resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==} + dev: false + + /uid2@1.0.0: + resolution: {integrity: sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ==} + engines: {node: '>= 4.0.0'} + dev: false + + /uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + dependencies: + '@lukeed/csprng': 1.1.0 + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + /unescape@1.0.1: + resolution: {integrity: sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 2.0.1 + dev: false + + /unicode-canonical-property-names-ecmascript@2.0.0: + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} + dev: true + + /unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-property-aliases-ecmascript: 2.1.0 + dev: true + + /unicode-match-property-value-ecmascript@2.1.0: + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} + dev: true + + /unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + dev: true + + /unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + dev: true + + /unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + dev: true + + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + dev: true + + /unix-crypt-td-js@1.1.4: + resolution: {integrity: sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==} + dev: true + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: true + + /unzipper@0.10.14: + resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} + dependencies: + big-integer: 1.6.52 + binary: 0.3.0 + bluebird: 3.4.7 + buffer-indexof-polyfill: 1.0.2 + duplexer2: 0.1.4 + fstream: 1.0.12 + graceful-fs: 4.2.11 + listenercount: 1.0.1 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + dev: false + + /update-browserslist-db@1.0.13(browserslist@4.23.0): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.23.0 + escalade: 3.1.2 + picocolors: 1.0.0 + dev: true + + /upper-case@1.1.3: + resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} + dev: false + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + + /urllib@2.41.0: + resolution: {integrity: sha512-pNXdxEv52L67jahLT+/7QE+Fup1y2Gc6EdmrAhQ6OpQIC2rl14oWwv9hvk1GXOZqEnJNwRXHABuwgPOs1CtL7g==} + engines: {node: '>= 0.10.0'} + peerDependencies: + proxy-agent: ^5.0.0 + peerDependenciesMeta: + proxy-agent: + optional: true + dependencies: + any-promise: 1.3.0 + content-type: 1.0.5 + debug: 2.6.9 + default-user-agent: 1.0.0 + digest-header: 1.1.0 + ee-first: 1.1.1 + formstream: 1.3.1 + humanize-ms: 1.2.1 + iconv-lite: 0.4.24 + ip: 1.1.8 + pump: 3.0.0 + qs: 6.11.2 + statuses: 1.5.0 + utility: 1.18.0 + transitivePeerDependencies: + - supports-color + dev: false + + /user-home@2.0.0: + resolution: {integrity: sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==} + engines: {node: '>=0.10.0'} + dependencies: + os-homedir: 1.0.2 + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.13 + which-typed-array: 1.1.15 + dev: false + + /utility@1.18.0: + resolution: {integrity: sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==} + engines: {node: '>= 0.12.0'} + dependencies: + copy-to: 2.0.1 + escape-html: 1.0.3 + mkdirp: 0.5.6 + mz: 2.7.0 + unescape: 1.0.1 + dev: false + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + /v8-to-istanbul@9.2.0: + resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.22 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + dev: true + + /valid-data-url@3.0.1: + resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==} + engines: {node: '>=10'} + dev: false + + /validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + dev: true + + /validator@13.11.0: + resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} + engines: {node: '>= 0.10'} + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + /vis@4.21.0-EOL: + resolution: {integrity: sha512-JVS1mywKg5S88XbkDJPfCb3n+vlg5fMA8Ae2hzs3KHAwD4ryM5qwlbFZ6ReDfY8te7I4NLCpuCoywJQEehvJlQ==} + deprecated: Please consider using https://github.com/visjs + dependencies: + emitter-component: 1.1.2 + hammerjs: 2.0.8 + keycharm: 0.2.0 + moment: 2.30.1 + propagating-hammerjs: 1.5.0 + dev: true + + /void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + requiresBuild: true + dev: false + + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + dependencies: + makeerror: 1.0.12 + dev: true + + /watchpack@2.4.0: + resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} + engines: {node: '>=10.13.0'} + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + dev: true + + /wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + dependencies: + defaults: 1.0.4 + dev: true + + /web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + dev: false + + /web-resource-inliner@6.0.1: + resolution: {integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==} + engines: {node: '>=10.0.0'} + dependencies: + ansi-colors: 4.1.3 + escape-goat: 3.0.0 + htmlparser2: 5.0.1 + mime: 2.6.0 + node-fetch: 2.7.0 + valid-data-url: 3.0.1 + transitivePeerDependencies: + - encoding + dev: false + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + /webpack-node-externals@3.0.0: + resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} + engines: {node: '>=6'} + dev: true + + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + dev: true + + /webpack@5.90.1: + resolution: {integrity: sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.5 + '@webassemblyjs/ast': 1.11.6 + '@webassemblyjs/wasm-edit': 1.11.6 + '@webassemblyjs/wasm-parser': 1.11.6 + acorn: 8.11.3 + acorn-import-assertions: 1.9.0(acorn@8.11.3) + browserslist: 4.23.0 + chrome-trace-event: 1.0.3 + enhanced-resolve: 5.15.0 + es-module-lexer: 1.4.1 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.10(webpack@5.90.1) + watchpack: 2.4.0 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + dev: true + + /websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + dependencies: + http-parser-js: 0.5.8 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + dev: true + + /websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + dev: true + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: false + + /which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + + /wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + requiresBuild: true + dependencies: + string-width: 4.2.3 + dev: false + optional: true + + /widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + dependencies: + string-width: 4.2.3 + dev: false + + /win-release@1.1.1: + resolution: {integrity: sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==} + engines: {node: '>=0.10.0'} + dependencies: + semver: 5.7.2 + dev: false + + /windows-release@4.0.0: + resolution: {integrity: sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg==} + engines: {node: '>=10'} + dependencies: + execa: 4.1.0 + dev: true + + /winston-daily-rotate-file@5.0.0(winston@3.11.0): + resolution: {integrity: sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==} + engines: {node: '>=8'} + peerDependencies: + winston: ^3 + dependencies: + file-stream-rotator: 0.6.1 + object-hash: 3.0.0 + triple-beam: 1.4.1 + winston: 3.11.0 + winston-transport: 4.7.0 + dev: false + + /winston-transport@4.7.0: + resolution: {integrity: sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==} + engines: {node: '>= 12.0.0'} + dependencies: + logform: 2.6.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + dev: false + + /winston@3.11.0: + resolution: {integrity: sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.5 + is-stream: 2.0.1 + logform: 2.6.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.4.3 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.7.0 + dev: false + + /with@7.0.2: + resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} + engines: {node: '>= 10.0.0'} + requiresBuild: true + dependencies: + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 + assert-never: 1.2.1 + babel-walk: 3.0.0-canary-5 + dev: false + + /word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + /wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + dev: true + + /ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + /xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + dependencies: + sax: 1.3.0 + xmlbuilder: 11.0.1 + dev: false + + /xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + dev: false + + /xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + dev: false + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: false + + /xmldoc@1.3.0: + resolution: {integrity: sha512-y7IRWW6PvEnYQZNZFMRLNJw+p3pezM4nKYPfr15g4OOW9i8VpeydycFuipE2297OvZnh3jSb2pxOt9QpkZUVng==} + dependencies: + sax: 1.3.0 + dev: true + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + requiresBuild: true + dev: false + optional: true + + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true + + /zepto@1.2.0: + resolution: {integrity: sha512-C1x6lfvBICFTQIMgbt3JqMOno3VOtkWat/xEakLTOurskYIHPmzJrzd1e8BnmtdDVJlGuk5D+FxyCA8MPmkIyA==} + dev: true + + /zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + dev: false diff --git a/public/upload/.gitkeep b/public/upload/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/genEnvTypes.ts b/scripts/genEnvTypes.ts new file mode 100644 index 0000000..1e90f3a --- /dev/null +++ b/scripts/genEnvTypes.ts @@ -0,0 +1,49 @@ +import fs from 'node:fs' +import path from 'node:path' + +import dotenv from 'dotenv' + +const directoryPath = path.resolve(__dirname, '..') + +const targets = ['.env', `.env.${process.env.NODE_ENV || 'development'}`] + +const envObj = targets.reduce((prev, file) => { + const result = dotenv.parse(fs.readFileSync(path.join(directoryPath, file))) + return { ...prev, ...result } +}, {}) + +const envType = Object.entries(envObj).reduce((prev, [key, value]) => { + return `${prev} + ${key}: '${value}';` +}, '').trim() + +fs.writeFile(path.join(directoryPath, 'types/env.d.ts'), ` +// generate by ./scripts/generateEnvTypes.ts +declare global { + namespace NodeJS { + interface ProcessEnv { + ${envType} + } + } +} +export {}; + `, (err) => { + if (err) + console.log('生成 env.d.ts 文件失败') + else + console.log('成功生成 env.d.ts 文件') +}) + +// console.log('envObj:', envObj) + +function formatValue(value) { + let _value + try { + const res = JSON.parse(value) + _value = typeof res === 'object' ? value : res + } + catch (error) { + _value = `'${value}'` + } + return _value +} diff --git a/scripts/resetScheduler.ts b/scripts/resetScheduler.ts new file mode 100644 index 0000000..2a85291 --- /dev/null +++ b/scripts/resetScheduler.ts @@ -0,0 +1,26 @@ +import { exec } from 'node:child_process' + +import { CronJob } from 'cron' + +/** 此文件仅供演示时使用 */ + +const runMigrationGenerate = async function () { + exec('npm run migration:revert && npm run migration:run', (error, stdout, stderr) => { + if (!error) + console.log('操作成功', error) + + else + console.log('操作失败', error) + }) +} + +const job = CronJob.from({ + /** 每天凌晨 4.30 恢复初始数据 */ + cronTime: '30 4 * * *', + timeZone: 'Asia/Shanghai', + start: true, + onTick() { + runMigrationGenerate() + console.log('Task executed daily at 4.30 AM:', new Date().toLocaleTimeString()) + }, +}) diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..53348e8 --- /dev/null +++ b/src/app.module.ts @@ -0,0 +1,98 @@ +import { ClassSerializerInterceptor, Module } from '@nestjs/common'; + +import { ConfigModule } from '@nestjs/config'; +import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; + +import config from '~/config'; +import { SharedModule } from '~/shared/shared.module'; + +import { AllExceptionsFilter } from './common/filters/any-exception.filter'; + +import { IdempotenceInterceptor } from './common/interceptors/idempotence.interceptor'; +import { TimeoutInterceptor } from './common/interceptors/timeout.interceptor'; +import { TransformInterceptor } from './common/interceptors/transform.interceptor'; +import { AuthModule } from './modules/auth/auth.module'; +import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard'; +import { RbacGuard } from './modules/auth/guards/rbac.guard'; +import { HealthModule } from './modules/health/health.module'; +import { NetdiskModule } from './modules/netdisk/netdisk.module'; +import { SseModule } from './modules/sse/sse.module'; +import { SystemModule } from './modules/system/system.module'; +import { TasksModule } from './modules/tasks/tasks.module'; +import { TodoModule } from './modules/todo/todo.module'; +import { ToolsModule } from './modules/tools/tools.module'; +import { DatabaseModule } from './shared/database/database.module'; + +import { SocketModule } from './socket/socket.module'; +import { ContractModule } from './modules/contract/contract.module'; +import { MaterialsInventoryModule } from './modules/materials_inventory/materials_inventory.module'; +import { CompanyModule } from './modules/company/company.module'; +import { ProductModule } from './modules/product/product.module'; +import { ProjectModule } from './modules/project/project.module'; +import { VehicleUsageModule } from './modules/vehicle_usage/vehicle_usage.module'; +import { SaleQuotationModule } from './modules/sale_quotation/sale_quotation.module'; +import { DomainModule } from './modules/domian/domain.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + expandVariables: true, + // 指定多个 env 文件时,第一个优先级最高 + envFilePath: ['.env.local', `.env.${process.env.NODE_ENV}`, '.env'], + load: [...Object.values(config)] + }), + SharedModule, + DatabaseModule, + + AuthModule, + SystemModule, + TasksModule.forRoot(), + ToolsModule, + SocketModule, + HealthModule, + SseModule, + NetdiskModule, + + // biz + + // end biz + + TodoModule, + // 合同模块 + ContractModule, + + // 原材料库存 + MaterialsInventoryModule, + + // 公司管理 + CompanyModule, + + // 产品管理 + ProductModule, + + // 项目管理 + ProjectModule, + + // 车辆管理 + VehicleUsageModule, + + //报价管理 + SaleQuotationModule, + //域 + DomainModule + ], + providers: [ + { provide: APP_FILTER, useClass: AllExceptionsFilter }, + + { provide: APP_INTERCEPTOR, useClass: ClassSerializerInterceptor }, + { provide: APP_INTERCEPTOR, useClass: TransformInterceptor }, + { provide: APP_INTERCEPTOR, useFactory: () => new TimeoutInterceptor(15 * 1000) }, + { provide: APP_INTERCEPTOR, useClass: IdempotenceInterceptor }, + + { provide: APP_GUARD, useClass: JwtAuthGuard }, + { provide: APP_GUARD, useClass: RbacGuard } + ], + controllers: [] +}) +export class AppModule {} diff --git a/src/assets/templates/verification-code-zh.hbs b/src/assets/templates/verification-code-zh.hbs new file mode 100644 index 0000000..df3702a --- /dev/null +++ b/src/assets/templates/verification-code-zh.hbs @@ -0,0 +1,4 @@ +

你的验证码是:

+

{{code}}

+

该验证码 10 分钟内有效,请勿将验证码告知给他人!

+本邮件由系统自动发出,请勿回复。 \ No newline at end of file diff --git a/src/assets/templates/verification-code.hbs b/src/assets/templates/verification-code.hbs new file mode 100644 index 0000000..6545c27 --- /dev/null +++ b/src/assets/templates/verification-code.hbs @@ -0,0 +1,5 @@ +

Your verification code is:

+

{{verificationCode}}

+

This code will expire in 10 minutes.

+This email is sent automatically by the system, please do not + reply. \ No newline at end of file diff --git a/src/common/adapters/fastify.adapter.ts b/src/common/adapters/fastify.adapter.ts new file mode 100644 index 0000000..25ff8c2 --- /dev/null +++ b/src/common/adapters/fastify.adapter.ts @@ -0,0 +1,45 @@ +import FastifyCookie from '@fastify/cookie'; +import FastifyMultipart from '@fastify/multipart'; +import { FastifyAdapter } from '@nestjs/platform-fastify'; + +const app: FastifyAdapter = new FastifyAdapter({ + trustProxy: true, + logger: false + // forceCloseConnections: true, +}); +export { app as fastifyApp }; + +app.register(FastifyMultipart, { + attachFieldsToBody:true, + limits: { + fields: 10, // Max number of non-file fields + fileSize: 1024 * 1024 * 50, // limit size 50M + files: 5 // Max number of file fields + } +}); + +app.register(FastifyCookie, { + secret: 'cookie-secret' // 这个 secret 不太重要,不存鉴权相关,无关紧要 +}); + +app.getInstance().addHook('onRequest', (request, reply, done) => { + // set undefined origin + const { origin } = request.headers; + if (!origin) request.headers.origin = request.headers.host; + + // forbidden php + + const { url } = request; + + if (url.endsWith('.php')) { + reply.raw.statusMessage = + 'Eh. PHP is not support on this machine. Yep, I also think PHP is bestest programming language. But for me it is beyond my reach.'; + + return reply.code(418).send(); + } + + // skip favicon request + if (url.match(/favicon.ico$/) || url.match(/manifest.json$/)) return reply.code(204).send(); + + done(); +}); diff --git a/src/common/adapters/socket.adapter.ts b/src/common/adapters/socket.adapter.ts new file mode 100644 index 0000000..ae18518 --- /dev/null +++ b/src/common/adapters/socket.adapter.ts @@ -0,0 +1,26 @@ +import { INestApplication } from '@nestjs/common'; +import { IoAdapter } from '@nestjs/platform-socket.io'; +import { createAdapter } from '@socket.io/redis-adapter'; + +import { REDIS_PUBSUB } from '~/shared/redis/redis.constant'; + +export const RedisIoAdapterKey = 'm-shop-socket'; + +export class RedisIoAdapter extends IoAdapter { + constructor(private readonly app: INestApplication) { + super(app); + } + + createIOServer(port: number, options?: any) { + const server = super.createIOServer(port, options); + + const { pubClient, subClient } = this.app.get(REDIS_PUBSUB); + + const redisAdapter = createAdapter(pubClient, subClient, { + key: RedisIoAdapterKey, + requestsTimeout: 10000 + }); + server.adapter(redisAdapter); + return server; + } +} diff --git a/src/common/decorators/api-result.decorator.ts b/src/common/decorators/api-result.decorator.ts new file mode 100644 index 0000000..dc54efe --- /dev/null +++ b/src/common/decorators/api-result.decorator.ts @@ -0,0 +1,78 @@ +import { HttpStatus, Type, applyDecorators } from '@nestjs/common'; +import { ApiExtraModels, ApiResponse, getSchemaPath } from '@nestjs/swagger'; + +import { ResOp } from '~/common/model/response.model'; + +const baseTypeNames = ['String', 'Number', 'Boolean']; + +function genBaseProp(type: Type) { + if (baseTypeNames.includes(type.name)) return { type: type.name.toLocaleLowerCase() }; + else return { $ref: getSchemaPath(type) }; +} + +/** + * @description: 生成返回结果装饰器 + */ +export function ApiResult>({ + type, + isPage, + status +}: { + type?: TModel | TModel[]; + isPage?: boolean; + status?: HttpStatus; +}) { + let prop = null; + + if (Array.isArray(type)) { + if (isPage) { + prop = { + type: 'object', + properties: { + items: { + type: 'array', + items: { $ref: getSchemaPath(type[0]) } + }, + meta: { + type: 'object', + properties: { + itemCount: { type: 'number', default: 0 }, + totalItems: { type: 'number', default: 0 }, + itemsPerPage: { type: 'number', default: 0 }, + totalPages: { type: 'number', default: 0 }, + currentPage: { type: 'number', default: 0 } + } + } + } + }; + } else { + prop = { + type: 'array', + items: genBaseProp(type[0]) + }; + } + } else if (type) { + prop = genBaseProp(type); + } else { + prop = { type: 'null', default: null }; + } + + const model = Array.isArray(type) ? type[0] : type; + + return applyDecorators( + ApiExtraModels(model), + ApiResponse({ + status, + schema: { + allOf: [ + { $ref: getSchemaPath(ResOp) }, + { + properties: { + data: prop + } + } + ] + } + }) + ); +} diff --git a/src/common/decorators/bypass.decorator.ts b/src/common/decorators/bypass.decorator.ts new file mode 100644 index 0000000..eb66876 --- /dev/null +++ b/src/common/decorators/bypass.decorator.ts @@ -0,0 +1,10 @@ +import { SetMetadata } from '@nestjs/common'; + +export const BYPASS_KEY = '__bypass_key__'; + +/** + * 当不需要转换成基础返回格式时添加该装饰器 + */ +export function Bypass() { + return SetMetadata(BYPASS_KEY, true); +} diff --git a/src/common/decorators/cookie.decorator.ts b/src/common/decorators/cookie.decorator.ts new file mode 100644 index 0000000..9b91ba5 --- /dev/null +++ b/src/common/decorators/cookie.decorator.ts @@ -0,0 +1,8 @@ +import type { ExecutionContext } from '@nestjs/common'; +import { createParamDecorator } from '@nestjs/common'; +import type { FastifyRequest } from 'fastify'; + +export const Cookies = createParamDecorator((data: string, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return data ? request.cookies?.[data] : request.cookies; +}); diff --git a/src/common/decorators/cron-once.decorator.ts b/src/common/decorators/cron-once.decorator.ts new file mode 100644 index 0000000..36267e9 --- /dev/null +++ b/src/common/decorators/cron-once.decorator.ts @@ -0,0 +1,19 @@ +import cluster from 'node:cluster'; + +import { Cron } from '@nestjs/schedule'; + +import { isMainProcess } from '~/global/env'; + +export const CronOnce: typeof Cron = (...rest): MethodDecorator => { + // If not in cluster mode, and PM2 main worker + if (isMainProcess) + // eslint-disable-next-line no-useless-call + return Cron.call(null, ...rest); + + if (cluster.isWorker && cluster.worker?.id === 1) + // eslint-disable-next-line no-useless-call + return Cron.call(null, ...rest); + + const returnNothing: MethodDecorator = () => {}; + return returnNothing; +}; diff --git a/src/common/decorators/domain.decorator.ts b/src/common/decorators/domain.decorator.ts new file mode 100644 index 0000000..1ecb045 --- /dev/null +++ b/src/common/decorators/domain.decorator.ts @@ -0,0 +1,18 @@ +import { ExecutionContext, createParamDecorator } from '@nestjs/common'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional } from 'class-validator'; +import type { FastifyRequest } from 'fastify'; +/** + * 当前域 + */ +export const Domain = createParamDecorator((_, context: ExecutionContext) => { + const request = context.switchToHttp().getRequest(); + return request.headers['sk-domain'] ?? 1; +}); + +export type SkDomain = number; +export class DomainType { + @ApiProperty({ description: '所属域' }) + @IsOptional() + domain: SkDomain; +} diff --git a/src/common/decorators/field.decorator.ts b/src/common/decorators/field.decorator.ts new file mode 100644 index 0000000..3423135 --- /dev/null +++ b/src/common/decorators/field.decorator.ts @@ -0,0 +1,108 @@ +import { applyDecorators } from '@nestjs/common'; +import { + IsBoolean, + IsDate, + IsInt, + IsNotEmpty, + IsNumber, + IsOptional, + IsPositive, + IsString, + Max, + MaxLength, + Min, + MinLength +} from 'class-validator'; +import { isNumber } from 'lodash'; + +import { + ToArray, + ToBoolean, + ToDate, + ToLowerCase, + ToNumber, + ToTrim, + ToUpperCase +} from './transform.decorator'; + +interface IOptionalOptions { + required?: boolean; +} + +interface INumberFieldOptions extends IOptionalOptions { + each?: boolean; + int?: boolean; + min?: number; + max?: number; + positive?: boolean; +} + +interface IStringFieldOptions extends IOptionalOptions { + each?: boolean; + minLength?: number; + maxLength?: number; + lowerCase?: boolean; + upperCase?: boolean; +} + +export function NumberField(options: INumberFieldOptions = {}): PropertyDecorator { + const { each, min, max, int, positive, required = true } = options; + + const decorators = [ToNumber()]; + + if (each) decorators.push(ToArray()); + + if (int) decorators.push(IsInt({ each })); + else decorators.push(IsNumber({}, { each })); + + if (isNumber(min)) decorators.push(Min(min, { each })); + + if (isNumber(max)) decorators.push(Max(max, { each })); + + if (positive) decorators.push(IsPositive({ each })); + + if (!required) decorators.push(IsOptional()); + + return applyDecorators(...decorators); +} + +export function StringField(options: IStringFieldOptions = {}): PropertyDecorator { + const { each, minLength, maxLength, lowerCase, upperCase, required = true } = options; + + const decorators = [IsString({ each }), ToTrim()]; + + if (each) decorators.push(ToArray()); + + if (isNumber(minLength)) decorators.push(MinLength(minLength, { each })); + + if (isNumber(maxLength)) decorators.push(MaxLength(maxLength, { each })); + + if (lowerCase) decorators.push(ToLowerCase()); + + if (upperCase) decorators.push(ToUpperCase()); + + if (!required) decorators.push(IsOptional()); + else decorators.push(IsNotEmpty({ each })); + + return applyDecorators(...decorators); +} + +export function BooleanField(options: IOptionalOptions = {}): PropertyDecorator { + const decorators = [ToBoolean(), IsBoolean()]; + + const { required = true } = options; + + if (!required) decorators.push(IsOptional()); + + return applyDecorators(...decorators); +} + +export function DateField(options: IOptionalOptions = {}): PropertyDecorator { + const decorators = [ToDate(), IsDate()]; + + const { required = true } = options; + + if (!required) decorators.push(IsOptional()); + + return applyDecorators(...decorators); +} diff --git a/src/common/decorators/http.decorator.ts b/src/common/decorators/http.decorator.ts new file mode 100644 index 0000000..c94d366 --- /dev/null +++ b/src/common/decorators/http.decorator.ts @@ -0,0 +1,31 @@ +import type { ExecutionContext } from '@nestjs/common'; + +import { createParamDecorator } from '@nestjs/common'; +import type { FastifyRequest } from 'fastify'; + +import { getIp, getIsMobile } from '~/utils/ip.util'; + + +/** + * 快速获取IP + */ +export const IsMobile = createParamDecorator((_, context: ExecutionContext) => { + const request = context.switchToHttp().getRequest(); + return getIsMobile(request); +}); + +/** + * 快速获取IP + */ +export const Ip = createParamDecorator((_, context: ExecutionContext) => { + const request = context.switchToHttp().getRequest(); + return getIp(request); +}); + +/** + * 快速获取request path,并不包括url params + */ +export const Uri = createParamDecorator((_, context: ExecutionContext) => { + const request = context.switchToHttp().getRequest(); + return request.routerPath; +}); diff --git a/src/common/decorators/id-param.decorator.ts b/src/common/decorators/id-param.decorator.ts new file mode 100644 index 0000000..eb41a79 --- /dev/null +++ b/src/common/decorators/id-param.decorator.ts @@ -0,0 +1,13 @@ +import { HttpStatus, NotAcceptableException, Param, ParseIntPipe } from '@nestjs/common'; + +export function IdParam() { + return Param( + 'id', + new ParseIntPipe({ + errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE, + exceptionFactory: _error => { + throw new NotAcceptableException('id 格式不正确'); + } + }) + ); +} diff --git a/src/common/decorators/idempotence.decorator.ts b/src/common/decorators/idempotence.decorator.ts new file mode 100644 index 0000000..949b321 --- /dev/null +++ b/src/common/decorators/idempotence.decorator.ts @@ -0,0 +1,15 @@ +import { SetMetadata } from '@nestjs/common'; + +import { IdempotenceOption } from '../interceptors/idempotence.interceptor'; + +export const HTTP_IDEMPOTENCE_KEY = '__idempotence_key__'; +export const HTTP_IDEMPOTENCE_OPTIONS = '__idempotence_options__'; + +/** + * 幂等 + */ +export function Idempotence(options?: IdempotenceOption): MethodDecorator { + return function (target, key, descriptor: PropertyDescriptor) { + SetMetadata(HTTP_IDEMPOTENCE_OPTIONS, options || {})(descriptor.value); + }; +} diff --git a/src/common/decorators/swagger.decorator.ts b/src/common/decorators/swagger.decorator.ts new file mode 100644 index 0000000..92744d8 --- /dev/null +++ b/src/common/decorators/swagger.decorator.ts @@ -0,0 +1,11 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiSecurity } from '@nestjs/swagger'; + +export const API_SECURITY_AUTH = 'auth'; + +/** + * like to @ApiSecurity('auth') + */ +export function ApiSecurityAuth(): ClassDecorator & MethodDecorator { + return applyDecorators(ApiSecurity(API_SECURITY_AUTH)); +} diff --git a/src/common/decorators/transform.decorator.ts b/src/common/decorators/transform.decorator.ts new file mode 100644 index 0000000..f69dc98 --- /dev/null +++ b/src/common/decorators/transform.decorator.ts @@ -0,0 +1,137 @@ +import { Transform } from 'class-transformer'; +import { castArray, isArray, isNil, trim } from 'lodash'; + +/** + * convert string to number + */ +export function ToNumber(): PropertyDecorator { + return Transform( + params => { + const value = params.value as string[] | string; + + if (isArray(value)) return value.map(v => Number(v)); + + return Number(value); + }, + { toClassOnly: true } + ); +} + +/** + * convert string to int + */ +export function ToInt(): PropertyDecorator { + return Transform( + params => { + const value = params.value as string[] | string; + + if (isArray(value)) return value.map(v => Number.parseInt(v)); + + return Number.parseInt(value); + }, + { toClassOnly: true } + ); +} + +/** + * convert string to boolean + */ +export function ToBoolean(): PropertyDecorator { + return Transform( + params => { + switch (params.value) { + case 'true': + return true; + case 'false': + return false; + default: + return params.value; + } + }, + { toClassOnly: true } + ); +} + +/** + * convert string to Date + */ +export function ToDate(): PropertyDecorator { + return Transform( + params => { + const { value } = params; + + if (!value) return; + + return new Date(value); + }, + { toClassOnly: true } + ); +} + +/** + * transforms to array, specially for query params + */ +export function ToArray(): PropertyDecorator { + return Transform( + params => { + const { value } = params; + + if (isNil(value)) return []; + + return castArray(value); + }, + { toClassOnly: true } + ); +} + +/** + * trim spaces from start and end, replace multiple spaces with one. + */ +export function ToTrim(): PropertyDecorator { + return Transform( + params => { + const value = params.value as string[] | string; + + if (isArray(value)) return value.map(v => trim(v)); + + return trim(value); + }, + { toClassOnly: true } + ); +} + +/** + * lowercase value + */ +export function ToLowerCase(): PropertyDecorator { + return Transform( + params => { + const value = params.value as string[] | string; + + if (!value) return; + + if (isArray(value)) return value.map(v => v.toLowerCase()); + + return value.toLowerCase(); + }, + { toClassOnly: true } + ); +} + +/** + * uppercase value + */ +export function ToUpperCase(): PropertyDecorator { + return Transform( + params => { + const value = params.value as string[] | string; + + if (!value) return; + + if (isArray(value)) return value.map(v => v.toUpperCase()); + + return value.toUpperCase(); + }, + { toClassOnly: true } + ); +} diff --git a/src/common/dto/cursor.dto.ts b/src/common/dto/cursor.dto.ts new file mode 100644 index 0000000..b3b0305 --- /dev/null +++ b/src/common/dto/cursor.dto.ts @@ -0,0 +1,26 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose, Transform } from 'class-transformer'; +import { IsInt, IsOptional, Max, Min } from 'class-validator'; + +export class CursorDto { + @ApiProperty({ minimum: 0, default: 0 }) + @Min(0) + @IsInt() + @Expose() + @IsOptional({ always: true }) + @Transform(({ value: val }) => (val ? Number.parseInt(val) : 0), { + toClassOnly: true + }) + cursor?: number; + + @ApiProperty({ minimum: 1, maximum: 100, default: 10 }) + @Min(1) + @Max(100) + @IsInt() + @IsOptional({ always: true }) + @Expose() + @Transform(({ value: val }) => (val ? Number.parseInt(val) : 10), { + toClassOnly: true + }) + limit?: number; +} diff --git a/src/common/dto/delete.dto.ts b/src/common/dto/delete.dto.ts new file mode 100644 index 0000000..6f87b9e --- /dev/null +++ b/src/common/dto/delete.dto.ts @@ -0,0 +1,8 @@ +import { IsDefined, IsNotEmpty, IsNumber } from 'class-validator'; + +export class BatchDeleteDto { + @IsDefined() + @IsNotEmpty() + @IsNumber({}, { each: true }) + ids: number[]; +} diff --git a/src/common/dto/id.dto.ts b/src/common/dto/id.dto.ts new file mode 100644 index 0000000..271bece --- /dev/null +++ b/src/common/dto/id.dto.ts @@ -0,0 +1,6 @@ +import { IsNumber } from 'class-validator'; + +export class IdDto { + @IsNumber() + id: number; +} diff --git a/src/common/dto/pager.dto.ts b/src/common/dto/pager.dto.ts new file mode 100644 index 0000000..11f9348 --- /dev/null +++ b/src/common/dto/pager.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose, Transform } from 'class-transformer'; +import { Allow, IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; + +export enum Order { + ASC = 'ASC', + DESC = 'DESC' +} + +export class PagerDto { + @ApiProperty({ minimum: 1, default: 1 }) + @Min(1) + @IsInt() + @Expose() + @IsOptional({ always: true }) + @Transform(({ value: val }) => (val ? Number.parseInt(val) : 1), { + toClassOnly: true + }) + page?: number; + + @ApiProperty({ minimum: 1, maximum: 100, default: 10 }) + @Min(1) + @Max(100) + @IsInt() + @IsOptional({ always: true }) + @Expose() + @Transform(({ value: val }) => (val ? Number.parseInt(val) : 10), { + toClassOnly: true + }) + pageSize?: number; + + @ApiProperty() + @IsString() + @IsOptional() + field?: string; // | keyof T + + @ApiProperty({ enum: Order }) + @IsEnum(Order) + @IsOptional() + @Transform(({ value }) => (value === 'asc' ? Order.ASC : Order.DESC)) + order?: Order; + + @Allow() + _t?: number; +} diff --git a/src/common/entity/common.entity.ts b/src/common/entity/common.entity.ts new file mode 100644 index 0000000..1c9ad3f --- /dev/null +++ b/src/common/entity/common.entity.ts @@ -0,0 +1,56 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import { + BaseEntity, + Column, + CreateDateColumn, + PrimaryGeneratedColumn, + UpdateDateColumn, + VirtualColumn +} from 'typeorm'; + +// 如果觉得前端转换时间太麻烦,并且不考虑通用性的话,可以在服务端进行转换,eg: @UpdateDateColumn({ name: 'updated_at', transformer }) +// const transformer: ValueTransformer = { +// to(value) { +// return value +// }, +// from(value) { +// return dayjs(value).format('YYYY-MM-DD HH:mm:ss') +// }, +// } + +export abstract class CommonEntity extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; + +} + +export abstract class CompleteEntity extends CommonEntity { + @ApiHideProperty() + @Exclude() + @Column({ name: 'create_by', update: false, comment: '创建者' }) + createBy: number; + + @ApiHideProperty() + @Exclude() + @Column({ name: 'update_by', comment: '更新者' }) + updateBy: number; + + /** + * 不会保存到数据库中的虚拟列,数据量大时可能会有性能问题,有性能要求请考虑在 service 层手动实现 + * @see https://typeorm.io/decorator-reference#virtualcolumn + */ + @ApiProperty({ description: '创建者' }) + @VirtualColumn({ query: alias => `SELECT username FROM sys_user WHERE id = ${alias}.create_by` }) + creator: string; + + @ApiProperty({ description: '更新者' }) + @VirtualColumn({ query: alias => `SELECT username FROM sys_user WHERE id = ${alias}.update_by` }) + updater: string; +} diff --git a/src/common/exceptions/biz.exception.ts b/src/common/exceptions/biz.exception.ts new file mode 100644 index 0000000..7b00f13 --- /dev/null +++ b/src/common/exceptions/biz.exception.ts @@ -0,0 +1,40 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; + +import { ErrorEnum } from '~/constants/error-code.constant'; +import { RESPONSE_SUCCESS_CODE } from '~/constants/response.constant'; + +export class BusinessException extends HttpException { + private errorCode: number; + + constructor(error: ErrorEnum | string) { + // 如果是非 ErrorEnum + if (!error.includes(':')) { + super( + HttpException.createBody({ + code: RESPONSE_SUCCESS_CODE, + message: error + }), + HttpStatus.OK + ); + this.errorCode = RESPONSE_SUCCESS_CODE; + return; + } + + const [code, message] = error.split(':'); + super( + HttpException.createBody({ + code, + message + }), + HttpStatus.BAD_REQUEST + ); + + this.errorCode = Number(code); + } + + getErrorCode(): number { + return this.errorCode; + } +} + +export { BusinessException as BizException }; diff --git a/src/common/exceptions/not-found.exception.ts b/src/common/exceptions/not-found.exception.ts new file mode 100644 index 0000000..95b034d --- /dev/null +++ b/src/common/exceptions/not-found.exception.ts @@ -0,0 +1,10 @@ +import { NotFoundException } from '@nestjs/common'; +import { sample } from 'lodash'; + +export const NotFoundMessage = ['404, Not Found']; + +export class CannotFindException extends NotFoundException { + constructor() { + super(sample(NotFoundMessage)); + } +} diff --git a/src/common/exceptions/socket.exception.ts b/src/common/exceptions/socket.exception.ts new file mode 100644 index 0000000..ca0cde3 --- /dev/null +++ b/src/common/exceptions/socket.exception.ts @@ -0,0 +1,38 @@ +import { HttpException } from '@nestjs/common'; +import { WsException } from '@nestjs/websockets'; + +import { ErrorEnum } from '~/constants/error-code.constant'; + +export class SocketException extends WsException { + private errorCode: number; + + constructor(message: string); + constructor(error: ErrorEnum); + constructor(...args: any) { + const error = args[0]; + if (typeof error === 'string') { + super( + HttpException.createBody({ + code: 0, + message: error + }) + ); + this.errorCode = 0; + return; + } + + const [code, message] = error.split(':'); + super( + HttpException.createBody({ + code, + message + }) + ); + + this.errorCode = Number(code); + } + + getErrorCode(): number { + return this.errorCode; + } +} diff --git a/src/common/filters/any-exception.filter.ts b/src/common/filters/any-exception.filter.ts new file mode 100644 index 0000000..aee7fd5 --- /dev/null +++ b/src/common/filters/any-exception.filter.ts @@ -0,0 +1,80 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, + Logger +} from '@nestjs/common'; +import { FastifyReply, FastifyRequest } from 'fastify'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; + +import { isDev } from '~/global/env'; + +interface myError { + readonly status: number; + readonly statusCode?: number; + + readonly message?: string; +} + +@Catch() +export class AllExceptionsFilter implements ExceptionFilter { + private readonly logger = new Logger(AllExceptionsFilter.name); + + constructor() { + this.registerCatchAllExceptionsHook(); + } + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const request = ctx.getRequest(); + const response = ctx.getResponse(); + + const url = request.raw.url!; + + const status = + exception instanceof HttpException + ? exception.getStatus() + : (exception as myError)?.status || + (exception as myError)?.statusCode || + HttpStatus.INTERNAL_SERVER_ERROR; + + let message = + (exception as any)?.response?.message || (exception as myError)?.message || `${exception}`; + + // 系统内部错误时 + if (status === HttpStatus.INTERNAL_SERVER_ERROR && !(exception instanceof BusinessException)) { + Logger.error(exception, undefined, 'Catch'); + + // 生产环境下隐藏错误信息 + if (!isDev) message = ErrorEnum.SERVER_ERROR?.split(':')[1]; + } else { + this.logger.warn(`错误信息:(${status}) ${message} Path: ${decodeURI(url)}`); + } + + const apiErrorCode: number = + exception instanceof BusinessException ? exception.getErrorCode() : status; + + // 返回基础响应结果 + const resBody: IBaseResponse = { + code: apiErrorCode, + message, + data: null + }; + + response.status(status).send(resBody); + } + + registerCatchAllExceptionsHook() { + process.on('unhandledRejection', reason => { + console.error('unhandledRejection: ', reason); + }); + + process.on('uncaughtException', err => { + console.error('uncaughtException: ', err); + }); + } +} diff --git a/src/common/interceptors/idempotence.interceptor.ts b/src/common/interceptors/idempotence.interceptor.ts new file mode 100644 index 0000000..0569aa6 --- /dev/null +++ b/src/common/interceptors/idempotence.interceptor.ts @@ -0,0 +1,134 @@ +import type { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; + +import { ConflictException, Injectable, SetMetadata } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import type { FastifyRequest } from 'fastify'; +import { catchError, tap } from 'rxjs'; + +import { CacheService } from '~/shared/redis/cache.service'; +import { hashString } from '~/utils'; +import { getIp } from '~/utils/ip.util'; +import { getRedisKey } from '~/utils/redis.util'; + +import { + HTTP_IDEMPOTENCE_KEY, + HTTP_IDEMPOTENCE_OPTIONS +} from '../decorators/idempotence.decorator'; + +const IdempotenceHeaderKey = 'x-idempotence'; + +export interface IdempotenceOption { + errorMessage?: string; + pendingMessage?: string; + + /** + * 如果重复请求的话,手动处理异常 + */ + handler?: (req: FastifyRequest) => any; + + /** + * 记录重复请求的时间 + * @default 60 + */ + expired?: number; + + /** + * 如果 header 没有幂等 key,根据 request 生成 key,如何生成这个 key 的方法 + */ + generateKey?: (req: FastifyRequest) => string; + + /** + * 仅读取 header 的 key,不自动生成 + * @default false + */ + disableGenerateKey?: boolean; +} + +@Injectable() +export class IdempotenceInterceptor implements NestInterceptor { + constructor( + private readonly reflector: Reflector, + private readonly cacheService: CacheService + ) {} + + async intercept(context: ExecutionContext, next: CallHandler) { + const request = context.switchToHttp().getRequest(); + + // skip Get 请求 + if (request.method.toUpperCase() === 'GET') return next.handle(); + + const handler = context.getHandler(); + const options: IdempotenceOption | undefined = this.reflector.get( + HTTP_IDEMPOTENCE_OPTIONS, + handler + ); + + if (!options) return next.handle(); + + const { + errorMessage = '相同请求成功后在 60 秒内只能发送一次', + pendingMessage = '相同请求正在处理中...', + handler: errorHandler, + expired = 60, + disableGenerateKey = false + } = options; + const redis = this.cacheService.getClient(); + + const idempotence = request.headers[IdempotenceHeaderKey] as string; + const key = disableGenerateKey + ? undefined + : options.generateKey + ? options.generateKey(request) + : this.generateKey(request); + + const idempotenceKey = + !!(idempotence || key) && getRedisKey(`idempotence:${idempotence || key}`); + + SetMetadata(HTTP_IDEMPOTENCE_KEY, idempotenceKey)(handler); + + if (idempotenceKey) { + const resultValue: '0' | '1' | null = (await redis.get(idempotenceKey)) as any; + if (resultValue !== null) { + if (errorHandler) return await errorHandler(request); + + const message = { + 1: errorMessage, + 0: pendingMessage + }[resultValue]; + throw new ConflictException(message); + } else { + await redis.set(idempotenceKey, '0', 'EX', expired); + } + } + return next.handle().pipe( + tap(async () => { + idempotenceKey && (await redis.set(idempotenceKey, '1', 'KEEPTTL')); + }), + catchError(async err => { + if (idempotenceKey) await redis.del(idempotenceKey); + + throw err; + }) + ); + } + + private generateKey(req: FastifyRequest) { + const { body, params, query = {}, headers, url } = req; + + const obj = { body, url, params, query } as any; + + const uuid = headers['x-uuid']; + if (uuid) { + obj.uuid = uuid; + } else { + const ua = headers['user-agent']; + const ip = getIp(req); + + if (!ua && !ip) return undefined; + + Object.assign(obj, { ua, ip }); + } + + return hashString(JSON.stringify(obj)); + } +} diff --git a/src/common/interceptors/logging.interceptor.ts b/src/common/interceptors/logging.interceptor.ts new file mode 100644 index 0000000..d9595f7 --- /dev/null +++ b/src/common/interceptors/logging.interceptor.ts @@ -0,0 +1,24 @@ +import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common'; +import { Observable, tap } from 'rxjs'; + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + private logger = new Logger(LoggingInterceptor.name, { timestamp: false }); + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const call$ = next.handle(); + const request = context.switchToHttp().getRequest(); + const content = `${request.method} -> ${request.url}`; + const isSse = request.headers.accept === 'text/event-stream'; + this.logger.debug(`+++ 请求:${content}`); + const now = Date.now(); + + return call$.pipe( + tap(() => { + if (isSse) return; + + this.logger.debug(`--- 响应:${content}${` +${Date.now() - now}ms`}`); + }) + ); + } +} diff --git a/src/common/interceptors/timeout.interceptor.ts b/src/common/interceptors/timeout.interceptor.ts new file mode 100644 index 0000000..7a1382b --- /dev/null +++ b/src/common/interceptors/timeout.interceptor.ts @@ -0,0 +1,25 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, + RequestTimeoutException +} from '@nestjs/common'; +import { Observable, TimeoutError, throwError } from 'rxjs'; +import { catchError, timeout } from 'rxjs/operators'; + +@Injectable() +export class TimeoutInterceptor implements NestInterceptor { + constructor(private readonly time: number = 10000) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + timeout(this.time), + catchError(err => { + if (err instanceof TimeoutError) return throwError(new RequestTimeoutException('请求超时')); + + return throwError(err); + }) + ); + } +} diff --git a/src/common/interceptors/transform.interceptor.ts b/src/common/interceptors/transform.interceptor.ts new file mode 100644 index 0000000..f52f8b9 --- /dev/null +++ b/src/common/interceptors/transform.interceptor.ts @@ -0,0 +1,39 @@ +import { + CallHandler, + ExecutionContext, + HttpStatus, + Injectable, + NestInterceptor +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { ResOp } from '~/common/model/response.model'; + +import { BYPASS_KEY } from '../decorators/bypass.decorator'; + +/** + * 统一处理返回接口结果,如果不需要则添加 @Bypass 装饰器 + */ +@Injectable() +export class TransformInterceptor implements NestInterceptor { + constructor(private readonly reflector: Reflector) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const bypass = this.reflector.get(BYPASS_KEY, context.getHandler()); + + if (bypass) return next.handle(); + + return next.handle().pipe( + map(data => { + // if (typeof data === 'undefined') { + // context.switchToHttp().getResponse().status(HttpStatus.NO_CONTENT); + // return data; + // } + + return new ResOp(HttpStatus.OK, data ?? null); + }) + ); + } +} diff --git a/src/common/model/response.model.ts b/src/common/model/response.model.ts new file mode 100644 index 0000000..b3f6330 --- /dev/null +++ b/src/common/model/response.model.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { RESPONSE_SUCCESS_CODE, RESPONSE_SUCCESS_MSG } from '~/constants/response.constant'; + +export class ResOp { + @ApiProperty({ type: 'object' }) + data?: T; + + @ApiProperty({ type: 'number', default: RESPONSE_SUCCESS_CODE }) + code: number; + + @ApiProperty({ type: 'string', default: RESPONSE_SUCCESS_MSG }) + message: string; + + constructor(code: number, data: T, message = RESPONSE_SUCCESS_MSG) { + this.code = code; + this.data = data; + this.message = message; + } + + static success(data?: T, message?: string) { + return new ResOp(RESPONSE_SUCCESS_CODE, data, message); + } + + static error(code: number, message) { + return new ResOp(code, {}, message); + } +} + +export class TreeResult { + @ApiProperty() + id: number; + + @ApiProperty() + parentId: number; + + @ApiProperty() + children?: TreeResult[]; +} diff --git a/src/common/pipes/parse-int.pipe.ts b/src/common/pipes/parse-int.pipe.ts new file mode 100644 index 0000000..c6d7227 --- /dev/null +++ b/src/common/pipes/parse-int.pipe.ts @@ -0,0 +1,12 @@ +import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; + +@Injectable() +export class ParseIntPipe implements PipeTransform { + transform(value: string, metadata: ArgumentMetadata): number { + const val = Number.parseInt(value, 10); + + if (Number.isNaN(val)) throw new BadRequestException('id validation failed'); + + return val; + } +} diff --git a/src/config/app.config.ts b/src/config/app.config.ts new file mode 100644 index 0000000..079e0ce --- /dev/null +++ b/src/config/app.config.ts @@ -0,0 +1,20 @@ +import { ConfigType, registerAs } from '@nestjs/config'; + +import { env, envNumber } from '~/global/env'; + +export const appRegToken = 'app'; + +export const AppConfig = registerAs(appRegToken, () => ({ + name: env('APP_NAME'), + port: envNumber('APP_PORT', 3000), + baseUrl: env('APP_BASE_URL'), + globalPrefix: env('GLOBAL_PREFIX', 'api'), + locale: env('APP_LOCALE', 'zh-CN'), + + logger: { + level: env('LOGGER_LEVEL'), + maxFiles: envNumber('LOGGER_MAX_FILES') + } +})); + +export type IAppConfig = ConfigType; diff --git a/src/config/database.config.ts b/src/config/database.config.ts new file mode 100644 index 0000000..02df861 --- /dev/null +++ b/src/config/database.config.ts @@ -0,0 +1,37 @@ +import { ConfigType, registerAs } from '@nestjs/config'; + +import { DataSource, DataSourceOptions } from 'typeorm'; + +import { env, envBoolean, envNumber } from '~/global/env'; + +// eslint-disable-next-line import/order +import dotenv from 'dotenv'; + +dotenv.config({ path: `.env.${process.env.NODE_ENV}` }); + +// 当前通过 npm scripts 执行的命令 +const currentScript = process.env.npm_lifecycle_event; + +const dataSourceOptions: DataSourceOptions = { + type: 'mysql', + host: env('DB_HOST', '127.0.0.1'), + port: envNumber('DB_PORT', 3306), + username: env('DB_USERNAME'), + password: env('DB_PASSWORD'), + database: env('DB_DATABASE'), + synchronize: envBoolean('DB_SYNCHRONIZE', false), + // 解决通过 pnpm migration:run 初始化数据时,遇到的 SET FOREIGN_KEY_CHECKS = 0; 等语句报错问题, 仅在执行数据迁移操作时设为 true + multipleStatements: currentScript === 'typeorm', + entities: ['dist/modules/**/*.entity{.ts,.js}'], + migrations: ['dist/migrations/*{.ts,.js}'], + subscribers: ['dist/modules/**/*.subscriber{.ts,.js}'] +}; +export const dbRegToken = 'database'; + +export const DatabaseConfig = registerAs(dbRegToken, (): DataSourceOptions => dataSourceOptions); + +export type IDatabaseConfig = ConfigType; + +const dataSource = new DataSource(dataSourceOptions); + +export default dataSource; diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..57bae35 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,37 @@ +import { AppConfig, IAppConfig, appRegToken } from './app.config'; +import { DatabaseConfig, IDatabaseConfig, dbRegToken } from './database.config'; +import { IMailerConfig, MailerConfig, mailerRegToken } from './mailer.config'; +import { IOssConfig, OssConfig, ossRegToken } from './oss.config'; +import { IRedisConfig, RedisConfig, redisRegToken } from './redis.config'; +import { ISecurityConfig, SecurityConfig, securityRegToken } from './security.config'; +import { ISwaggerConfig, SwaggerConfig, swaggerRegToken } from './swagger.config'; + +export * from './app.config'; +export * from './redis.config'; +export * from './database.config'; +export * from './swagger.config'; +export * from './security.config'; +export * from './mailer.config'; +export * from './oss.config'; + +export interface AllConfigType { + [appRegToken]: IAppConfig; + [dbRegToken]: IDatabaseConfig; + [mailerRegToken]: IMailerConfig; + [redisRegToken]: IRedisConfig; + [securityRegToken]: ISecurityConfig; + [swaggerRegToken]: ISwaggerConfig; + [ossRegToken]: IOssConfig; +} + +export type ConfigKeyPaths = RecordNamePaths; + +export default { + AppConfig, + DatabaseConfig, + MailerConfig, + OssConfig, + RedisConfig, + SecurityConfig, + SwaggerConfig +}; diff --git a/src/config/mailer.config.ts b/src/config/mailer.config.ts new file mode 100644 index 0000000..6e17271 --- /dev/null +++ b/src/config/mailer.config.ts @@ -0,0 +1,18 @@ +import { ConfigType, registerAs } from '@nestjs/config'; + +import { env, envNumber } from '~/global/env'; + +export const mailerRegToken = 'mailer'; + +export const MailerConfig = registerAs(mailerRegToken, () => ({ + host: env('SMTP_HOST'), + port: envNumber('SMTP_PORT'), + ignoreTLS: true, + secure: true, + auth: { + user: env('SMTP_USER'), + pass: env('SMTP_PASS') + } +})); + +export type IMailerConfig = ConfigType; diff --git a/src/config/oss.config.ts b/src/config/oss.config.ts new file mode 100644 index 0000000..42f46c0 --- /dev/null +++ b/src/config/oss.config.ts @@ -0,0 +1,34 @@ +import { ConfigType, registerAs } from '@nestjs/config'; +import * as qiniu from 'qiniu'; + +import { env, envBoolean, envNumber } from '~/global/env'; + +function parseZone(zone: string) { + switch (zone) { + case 'Zone_as0': + return qiniu.zone.Zone_as0; + case 'Zone_na0': + return qiniu.zone.Zone_na0; + case 'Zone_z0': + return qiniu.zone.Zone_z0; + case 'Zone_z1': + return qiniu.zone.Zone_z1; + case 'Zone_z2': + return qiniu.zone.Zone_z2; + } +} + +export const ossRegToken = 'oss'; + +export const OssConfig = registerAs(ossRegToken, () => ({ + accessKey: env('OSS_ACCESSKEY'), + secretKey: env('OSS_SECRETKEY'), + domain: env('OSS_DOMAIN'), + port: envNumber('OSS_PORT'), + useSSL: envBoolean('OSS_USE_SSL'), + bucket: env('OSS_BUCKET'), + zone: parseZone(env('OSS_ZONE') || 'Zone_z2'), + access: (env('OSS_ACCESS_TYPE') as any) || 'public' +})); + +export type IOssConfig = ConfigType; diff --git a/src/config/redis.config.ts b/src/config/redis.config.ts new file mode 100644 index 0000000..3260bc4 --- /dev/null +++ b/src/config/redis.config.ts @@ -0,0 +1,14 @@ +import { ConfigType, registerAs } from '@nestjs/config'; + +import { env, envNumber } from '~/global/env'; + +export const redisRegToken = 'redis'; + +export const RedisConfig = registerAs(redisRegToken, () => ({ + host: env('REDIS_HOST', '127.0.0.1'), + port: envNumber('REDIS_PORT', 6379), + password: env('REDIS_PASSWORD'), + db: envNumber('REDIS_DB') +})); + +export type IRedisConfig = ConfigType; diff --git a/src/config/security.config.ts b/src/config/security.config.ts new file mode 100644 index 0000000..fb2f1b1 --- /dev/null +++ b/src/config/security.config.ts @@ -0,0 +1,14 @@ +import { ConfigType, registerAs } from '@nestjs/config'; + +import { env, envNumber } from '~/global/env'; + +export const securityRegToken = 'security'; + +export const SecurityConfig = registerAs(securityRegToken, () => ({ + jwtSecret: env('JWT_SECRET'), + jwtExprire: envNumber('JWT_EXPIRE'), + refreshSecret: env('REFRESH_TOKEN_SECRET'), + refreshExpire: envNumber('REFRESH_TOKEN_EXPIRE') +})); + +export type ISecurityConfig = ConfigType; diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts new file mode 100644 index 0000000..efbbb1a --- /dev/null +++ b/src/config/swagger.config.ts @@ -0,0 +1,12 @@ +import { ConfigType, registerAs } from '@nestjs/config'; + +import { env, envBoolean } from '~/global/env'; + +export const swaggerRegToken = 'swagger'; + +export const SwaggerConfig = registerAs(swaggerRegToken, () => ({ + enable: envBoolean('SWAGGER_ENABLE'), + path: env('SWAGGER_PATH') +})); + +export type ISwaggerConfig = ConfigType; diff --git a/src/constants/cache.constant.ts b/src/constants/cache.constant.ts new file mode 100644 index 0000000..858bf7f --- /dev/null +++ b/src/constants/cache.constant.ts @@ -0,0 +1,8 @@ +export enum RedisKeys { + AccessIp = 'access_ip', + CAPTCHA_IMG_PREFIX = 'captcha:img:', + AUTH_TOKEN_PREFIX = 'auth:token:', + AUTH_PERM_PREFIX = 'auth:permission:', + AUTH_PASSWORD_V_PREFIX = 'auth:passwordVersion:' +} +export const API_CACHE_PREFIX = 'api-cache:'; diff --git a/src/constants/enum/index.ts b/src/constants/enum/index.ts new file mode 100644 index 0000000..f150c1c --- /dev/null +++ b/src/constants/enum/index.ts @@ -0,0 +1,48 @@ +// 字典项status +export enum DictTypeStatusEnum { + /** 启用 */ + ENABLE = 1, + /** 禁用 */ + DISABLE = 0 +} + +// 业务模块枚举 +export enum BusinessModuleEnum { + CONTRACT = 1, + MATERIALS_INVENTORY = 2, + COMPANY = 3 +} + +// 原材料出库或者入库 +export enum MaterialsInOrOutEnum { + In, + Out +} + +// 系统参数key +export enum ParamConfigEnum { + InventoryNumberPrefix = 'inventory_number_prefix', + InventoryInOutNumberPrefixIn = 'inventory_inout_number_prefix_in', + InventoryInOutNumberPrefixOut = 'inventory_inout_number_prefix_out', + ProductNumberPrefix = 'product_number_prefix' +} + +// 合同审核状态 +export enum ContractStatusEnum { + Pending = 0, // 待审核 + Approved = 1, // 已通过 + Rejected = 2 // 已拒绝 +} + +// 库存查询剩余咋黄台 +export enum HasInventoryStatusEnum { + All = 0, // 全部 + Yes = 1, // 有库存 + No = 2 // 无库存 +} + +// 权限资源设备类型 +export enum ResourceDeviceEnum { + APP = 0, + PC = 1 +} diff --git a/src/constants/error-code.constant.ts b/src/constants/error-code.constant.ts new file mode 100644 index 0000000..61b5117 --- /dev/null +++ b/src/constants/error-code.constant.ts @@ -0,0 +1,72 @@ +export enum ErrorEnum { + DEFAULT = '0:未知错误', + SERVER_ERROR = '500:服务繁忙,请稍后再试', + + SYSTEM_USER_EXISTS = '1001:系统用户已存在', + INVALID_VERIFICATION_CODE = '1002:验证码填写有误', + INVALID_USERNAME_PASSWORD = '1003:用户名密码有误', + NODE_ROUTE_EXISTS = '1004:节点路由已存在', + PERMISSION_REQUIRES_PARENT = '1005:权限必须包含父节点', + ILLEGAL_OPERATION_DIRECTORY_PARENT = '1006:非法操作:该节点仅支持目录类型父节点', + ILLEGAL_OPERATION_CANNOT_CONVERT_NODE_TYPE = '1007:非法操作:节点类型无法直接转换', + ROLE_HAS_ASSOCIATED_USERS = '1008:该角色存在关联用户,请先删除关联用户', + DEPARTMENT_HAS_ASSOCIATED_USERS = '1009:该部门存在关联用户,请先删除关联用户', + DEPARTMENT_HAS_ASSOCIATED_ROLES = '1010:该部门存在关联角色,请先删除关联角色', + PASSWORD_MISMATCH = '1011:旧密码与原密码不一致', + LOGOUT_OWN_SESSION = '1012:如想下线自身可右上角退出', + NOT_ALLOWED_TO_LOGOUT_USER = '1013:不允许下线该用户', + PARENT_MENU_NOT_FOUND = '1014:父级菜单不存在', + DEPARTMENT_HAS_CHILD_DEPARTMENTS = '1015:该部门存在子部门,请先删除子部门', + SYSTEM_BUILTIN_FUNCTION_NOT_ALLOWED = '1016:系统内置功能不允许操作', + USER_NOT_FOUND = '1017:用户不存在', + UNABLE_TO_FIND_DEPARTMENT_FOR_USER = '1018:无法查找当前用户所属部门', + DEPARTMENT_NOT_FOUND = '1019:部门不存在', + DICT_NAME_EXISTS = '1020: 已存在相同名称的字典', + PARAMETER_CONFIG_KEY_EXISTS = '1021:参数配置键值对已存在', + DEFAULT_ROLE_NOT_FOUND = '1022:所分配的默认角色不存在', + + INVALID_LOGIN = '1101:登录无效,请重新登录', + NO_PERMISSION = '1102:无权限访问', + ONLY_ADMIN_CAN_LOGIN = '1103:不是管理员,无法登录', + REQUEST_INVALIDATED = '1104:当前请求已失效', + ACCOUNT_LOGGED_IN_ELSEWHERE = '1105:您的账号已在其他地方登录', + GUEST_ACCOUNT_RESTRICTED_OPERATION = '1106:游客账号不允许操作', + REQUESTED_RESOURCE_NOT_FOUND = '1107:所请求的资源不存在', + + TOO_MANY_REQUESTS = '1201:请求频率过快,请一分钟后再试', + MAXIMUM_FIVE_VERIFICATION_CODES_PER_DAY = '1202:一天最多发送5条验证码', + VERIFICATION_CODE_SEND_FAILED = '1203:验证码发送失败', + + INSECURE_MISSION = '1301:不安全的任务,确保执行的加入@Mission注解', + EXECUTED_MISSION_NOT_FOUND = '1302:所执行的任务不存在', + MISSION_EXECUTION_FAILED = '1303:任务执行失败', + MISSION_NOT_FOUND = '1304:任务不存在', + + // OSS相关 + OSS_FILE_OR_DIR_EXIST = '1401:当前创建的文件或目录已存在', + OSS_NO_OPERATION_REQUIRED = '1402:无需操作', + OSS_EXCEE_MAXIMUM_QUANTITY = '1403:已超出支持的最大处理数量', + + // Storage相关 + STORAGE_NOT_FOUND = '1404:文件不存在,请重试', + STORAGE_REFRENCE_EXISTS = '1405:文件存在关联,无法删除,请先找到该文件关联的业务解除关联。', + + // Product + PRODUCT_EXIST = '1406:产品已存在', + + // Contract + CONTRACT_NUMBER_EXIST = '1407:存在相同的合同编号', + + // Inventory + INVENTORY_INSUFFICIENT = '1408:库存数量不足。请检查库存或重新操作', + MATERIALS_IN_OUT_NOT_FOUND = '1409:出入库信息不存在', + MATERIALS_IN_OUT_UNIT_PRICE_CANNOT_BE_MODIFIED = '1410:该价格的产品已经出库,单价不允许修改。若有疑问,请联系管理员', + MATERIALS_IN_OUT_UNIT_PRICE_MUST_ZERO_WHEN_MODIFIED = '1411:只能修改初始单价为0的入库记录。 若有疑问,请联系管理员', + + // SaleQuotation + SALE_QUOTATION_COMPONENT_DUPLICATED = '1412:存在名称,价格,规格都相同的配件,请检查是否重复录入', + SALE_QUOTATION_TEMPLATE_NAME_DUPLICATE = '1413:模板名已存在', + + //domain + DOMAIN_TITLE_DUPLICATE = '1414:域标题已存在' +} diff --git a/src/constants/event-bus.constant.ts b/src/constants/event-bus.constant.ts new file mode 100644 index 0000000..ad5e7e7 --- /dev/null +++ b/src/constants/event-bus.constant.ts @@ -0,0 +1,4 @@ +export enum EventBusEvents { + TokenExpired = 'token.expired', + SystemException = 'system.exception' +} diff --git a/src/constants/oss.constant.ts b/src/constants/oss.constant.ts new file mode 100644 index 0000000..ceecb80 --- /dev/null +++ b/src/constants/oss.constant.ts @@ -0,0 +1,8 @@ +export const OSS_CONFIG = 'admin_module:qiniu_config'; +export const OSS_API = 'http://api.qiniu.com'; + +// 目录分隔符 +export const NETDISK_DELIMITER = '/'; +export const NETDISK_LIMIT = 100; +export const NETDISK_HANDLE_MAX_ITEM = 1000; +export const NETDISK_COPY_SUFFIX = '的副本'; diff --git a/src/constants/response.constant.ts b/src/constants/response.constant.ts new file mode 100644 index 0000000..a34fb9b --- /dev/null +++ b/src/constants/response.constant.ts @@ -0,0 +1,15 @@ +export const RESPONSE_SUCCESS_CODE = 200; + +export const RESPONSE_SUCCESS_MSG = 'success'; + +/** + * @description: contentType + */ +export enum ContentTypeEnum { + // json + JSON = 'application/json;charset=UTF-8', + // form-data qs + FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8', + // form-data upload + FORM_DATA = 'multipart/form-data;charset=UTF-8' +} diff --git a/src/constants/system.constant.ts b/src/constants/system.constant.ts new file mode 100644 index 0000000..d1e191c --- /dev/null +++ b/src/constants/system.constant.ts @@ -0,0 +1,6 @@ +export const SYS_USER_INITPASSWORD = 'sys_user_initPassword'; +export const SYS_API_TOKEN = 'sys_api_token'; +/** 超级管理员用户 id */ +export const ROOT_USER_ID = 1; +/** 超级管理员角色 id */ +export const ROOT_ROLE_ID = 1; diff --git a/src/global/env.ts b/src/global/env.ts new file mode 100644 index 0000000..4e283c0 --- /dev/null +++ b/src/global/env.ts @@ -0,0 +1,62 @@ +import cluster from 'node:cluster'; + +export const isMainCluster = + process.env.NODE_APP_INSTANCE && Number.parseInt(process.env.NODE_APP_INSTANCE) === 0; +export const isMainProcess = cluster.isPrimary || isMainCluster; + +export const isDev = process.env.NODE_ENV === 'development'; + +export const isTest = !!process.env.TEST; +export const cwd = process.cwd(); + +/** + * 基础类型接口 + */ +export type BaseType = boolean | number | string | undefined | null; + +/** + * 格式化环境变量 + * @param key 环境变量的键值 + * @param defaultValue 默认值 + * @param callback 格式化函数 + */ +function fromatValue( + key: string, + defaultValue: T, + callback?: (value: string) => T +): T { + const value: string | undefined = process.env[key]; + if (typeof value === 'undefined') return defaultValue; + + if (!callback) return value as unknown as T; + + return callback(value); +} + +export function env(key: string, defaultValue: string = '') { + return fromatValue(key, defaultValue); +} + +export function envString(key: string, defaultValue: string = '') { + return fromatValue(key, defaultValue); +} + +export function envNumber(key: string, defaultValue: number = 0) { + return fromatValue(key, defaultValue, value => { + try { + return Number(value); + } catch { + throw new Error(`${key} environment variable is not a number`); + } + }); +} + +export function envBoolean(key: string, defaultValue: boolean = false) { + return fromatValue(key, defaultValue, value => { + try { + return Boolean(JSON.parse(value)); + } catch { + throw new Error(`${key} environment variable is not a boolean`); + } + }); +} diff --git a/src/helper/catchError.ts b/src/helper/catchError.ts new file mode 100644 index 0000000..78b8502 --- /dev/null +++ b/src/helper/catchError.ts @@ -0,0 +1,5 @@ +export function catchError() { + process.on('unhandledRejection', (reason, p) => { + console.log('Promise: ', p, 'Reason: ', reason); + }); +} diff --git a/src/helper/crud/base.service.ts b/src/helper/crud/base.service.ts new file mode 100644 index 0000000..41c526b --- /dev/null +++ b/src/helper/crud/base.service.ts @@ -0,0 +1,35 @@ +import { NotFoundException } from '@nestjs/common'; +import { ObjectLiteral, Repository } from 'typeorm'; + +import { PagerDto } from '~/common/dto/pager.dto'; + +import { paginate } from '../paginate'; +import { Pagination } from '../paginate/pagination'; + +export class BaseService = Repository> { + constructor(private repository: R) {} + + async list({ page, pageSize }: PagerDto): Promise> { + return paginate(this.repository, { page, pageSize }); + } + + async findOne(id: number): Promise { + const item = await this.repository.createQueryBuilder().where({ id }).getOne(); + if (!item) throw new NotFoundException('未找到该记录'); + + return item; + } + + async create(dto: any): Promise { + return await this.repository.save(dto); + } + + async update(id: number, dto: any): Promise { + await this.repository.update(id, dto); + } + + async delete(id: number): Promise { + const item = await this.findOne(id); + await this.repository.remove(item); + } +} diff --git a/src/helper/crud/crud.factory.ts b/src/helper/crud/crud.factory.ts new file mode 100644 index 0000000..af72c80 --- /dev/null +++ b/src/helper/crud/crud.factory.ts @@ -0,0 +1,80 @@ +import type { Type } from '@nestjs/common'; + +import { Body, Controller, Delete, Get, Patch, Post, Put, Query } from '@nestjs/common'; +import { ApiBody, IntersectionType, PartialType } from '@nestjs/swagger'; +import pluralize from 'pluralize'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { PagerDto } from '~/common/dto/pager.dto'; + +import { BaseService } from './base.service'; + +export function BaseCrudFactory any>({ + entity, + dto, + permissions +}: { + entity: E; + dto?: Type; + permissions?: Record; +}): Type { + const prefix = entity.name.toLowerCase().replace(/entity$/, ''); + const pluralizeName = pluralize(prefix) as string; + + dto = dto ?? class extends entity {}; + + class Dto extends dto {} + class UpdateDto extends PartialType(Dto) {} + class QueryDto extends IntersectionType(PagerDto, PartialType(Dto)) {} + + permissions = + permissions ?? + ({ + LIST: `${prefix}:list`, + CREATE: `${prefix}:create`, + READ: `${prefix}:read`, + UPDATE: `${prefix}:update`, + DELETE: `${prefix}:delete` + } as const); + + @Controller(pluralizeName) + class BaseController> { + constructor(private service: S) {} + + @Get() + @ApiResult({ type: [entity], isPage: true }) + async list(@Query() pager: QueryDto) { + return await this.service.list(pager); + } + + @Get(':id') + @ApiResult({ type: entity }) + async get(@IdParam() id: number) { + return await this.service.findOne(id); + } + + @Post() + @ApiBody({ type: dto }) + async create(@Body() dto: Dto) { + return await this.service.create(dto); + } + + @Put(':id') + async update(@IdParam() id: number, @Body() dto: UpdateDto) { + return await this.service.update(id, dto); + } + + @Patch(':id') + async patch(@IdParam() id: number, @Body() dto: UpdateDto) { + await this.service.update(id, dto); + } + + @Delete(':id') + async delete(@IdParam() id: number) { + await this.service.delete(id); + } + } + + return BaseController; +} diff --git a/src/helper/genRedisKey.ts b/src/helper/genRedisKey.ts new file mode 100644 index 0000000..4cbe485 --- /dev/null +++ b/src/helper/genRedisKey.ts @@ -0,0 +1,19 @@ +import { RedisKeys } from '~/constants/cache.constant'; + +/** 生成验证码 redis key */ +export function genCaptchaImgKey(val: string | number) { + return `${RedisKeys.CAPTCHA_IMG_PREFIX}${String(val)}` as const; +} + +/** 生成 auth token redis key */ +export function genAuthTokenKey(val: string | number) { + return `${RedisKeys.AUTH_TOKEN_PREFIX}${String(val)}` as const; +} +/** 生成 auth permission redis key */ +export function genAuthPermKey(val: string | number) { + return `${RedisKeys.AUTH_PERM_PREFIX}${String(val)}` as const; +} +/** 生成 auth passwordVersion redis key */ +export function genAuthPVKey(val: string | number) { + return `${RedisKeys.AUTH_PASSWORD_V_PREFIX}${String(val)}` as const; +} diff --git a/src/helper/paginate/create-pagination.ts b/src/helper/paginate/create-pagination.ts new file mode 100644 index 0000000..2adfa11 --- /dev/null +++ b/src/helper/paginate/create-pagination.ts @@ -0,0 +1,26 @@ +import { IPaginationMeta } from './interface'; +import { Pagination } from './pagination'; + +export function createPaginationObject({ + items, + totalItems, + currentPage, + limit +}: { + items: T[]; + totalItems?: number; + currentPage: number; + limit: number; +}): Pagination { + const totalPages = totalItems !== undefined ? Math.ceil(totalItems / limit) : undefined; + + const meta: IPaginationMeta = { + totalItems, + itemCount: items.length, + itemsPerPage: limit, + totalPages, + currentPage + }; + + return new Pagination(items, meta); +} diff --git a/src/helper/paginate/index.ts b/src/helper/paginate/index.ts new file mode 100644 index 0000000..181243d --- /dev/null +++ b/src/helper/paginate/index.ts @@ -0,0 +1,141 @@ +import { + FindManyOptions, + FindOptionsWhere, + ObjectLiteral, + Repository, + SelectQueryBuilder +} from 'typeorm'; + +import { createPaginationObject } from './create-pagination'; +import { IPaginationOptions, PaginationTypeEnum } from './interface'; +import { Pagination } from './pagination'; + +const DEFAULT_LIMIT = 10; +const DEFAULT_PAGE = 1; + +function resolveOptions(options: IPaginationOptions): [number, number, PaginationTypeEnum] { + const { page, pageSize, paginationType } = options; + + return [ + page || DEFAULT_PAGE, + pageSize || DEFAULT_LIMIT, + paginationType || PaginationTypeEnum.TAKE_AND_SKIP + ]; +} + +async function paginateRepository( + repository: Repository, + options: IPaginationOptions, + searchOptions?: FindOptionsWhere | FindManyOptions +): Promise> { + const [page, limit] = resolveOptions(options); + + const promises: [Promise, Promise | undefined] = [ + repository.find({ + skip: limit * (page - 1), + take: limit, + ...searchOptions + }), + undefined + ]; + + const [items, total] = await Promise.all(promises); + + return createPaginationObject({ + items, + totalItems: total, + currentPage: page, + limit + }); +} + +async function paginateQueryBuilder( + queryBuilder: SelectQueryBuilder, + options: IPaginationOptions +): Promise> { + const [page, limit, paginationType] = resolveOptions(options); + + if (paginationType === PaginationTypeEnum.TAKE_AND_SKIP) + queryBuilder.take(limit).skip((page - 1) * limit); + else queryBuilder.limit(limit).offset((page - 1) * limit); + + const [items, total] = await queryBuilder.getManyAndCount(); + + return createPaginationObject({ + items, + totalItems: total, + currentPage: page, + limit + }); +} + +export async function paginateRaw( + queryBuilder: SelectQueryBuilder, + options: IPaginationOptions +): Promise> { + const [page, limit, paginationType] = resolveOptions(options); + + const promises: [Promise, Promise | undefined] = [ + (paginationType === PaginationTypeEnum.LIMIT_AND_OFFSET + ? queryBuilder.limit(limit).offset((page - 1) * limit) + : queryBuilder.take(limit).skip((page - 1) * limit) + ).getRawMany(), + queryBuilder.getCount() + ]; + + const [items, total] = await Promise.all(promises); + + return createPaginationObject({ + items, + totalItems: total, + currentPage: page, + limit + }); +} + +export async function paginateRawAndEntities( + queryBuilder: SelectQueryBuilder, + options: IPaginationOptions +): Promise<[Pagination, Partial[]]> { + const [page, limit, paginationType] = resolveOptions(options); + + const promises: [Promise<{ entities: T[]; raw: T[] }>, Promise | undefined] = [ + (paginationType === PaginationTypeEnum.LIMIT_AND_OFFSET + ? queryBuilder.limit(limit).offset((page - 1) * limit) + : queryBuilder.take(limit).skip((page - 1) * limit) + ).getRawAndEntities(), + queryBuilder.getCount() + ]; + + const [itemObject, total] = await Promise.all(promises); + + return [ + createPaginationObject({ + items: itemObject.entities, + totalItems: total, + currentPage: page, + limit + }), + itemObject.raw + ]; +} + +export async function paginate( + repository: Repository, + options: IPaginationOptions, + searchOptions?: FindOptionsWhere | FindManyOptions +): Promise>; +export async function paginate( + queryBuilder: SelectQueryBuilder, + options: IPaginationOptions +): Promise>; + +export async function paginate( + repositoryOrQueryBuilder: Repository | SelectQueryBuilder, + options: IPaginationOptions, + searchOptions?: FindOptionsWhere | FindManyOptions +) { + return repositoryOrQueryBuilder instanceof Repository + ? paginateRepository(repositoryOrQueryBuilder, options, searchOptions) + : paginateQueryBuilder(repositoryOrQueryBuilder, options); +} diff --git a/src/helper/paginate/interface.ts b/src/helper/paginate/interface.ts new file mode 100644 index 0000000..c7d3062 --- /dev/null +++ b/src/helper/paginate/interface.ts @@ -0,0 +1,27 @@ +import { ObjectLiteral } from 'typeorm'; + +export enum PaginationTypeEnum { + LIMIT_AND_OFFSET = 'limit', + TAKE_AND_SKIP = 'take' +} + +export interface IPaginationOptions { + page: number; + pageSize: number; + paginationType?: PaginationTypeEnum; +} + +export interface IPaginationMeta extends ObjectLiteral { + itemCount: number; + totalItems?: number; + itemsPerPage: number; + totalPages?: number; + currentPage: number; +} + +export interface IPaginationLinks { + first?: string; + previous?: string; + next?: string; + last?: string; +} diff --git a/src/helper/paginate/pagination.ts b/src/helper/paginate/pagination.ts new file mode 100644 index 0000000..02d97c2 --- /dev/null +++ b/src/helper/paginate/pagination.ts @@ -0,0 +1,11 @@ +import { ObjectLiteral } from 'typeorm'; + +import { IPaginationMeta } from './interface'; + +export class Pagination { + constructor( + public items: PaginationObject[], + + public readonly meta: T + ) {} +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..51a2048 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,88 @@ +import cluster from 'node:cluster'; +import path from 'node:path'; + +import { HttpStatus, Logger, UnprocessableEntityException, ValidationPipe } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { NestFactory } from '@nestjs/core'; +import { NestFastifyApplication } from '@nestjs/platform-fastify'; + +import { useContainer } from 'class-validator'; + +import { AppModule } from './app.module'; + +import { fastifyApp } from './common/adapters/fastify.adapter'; +import { RedisIoAdapter } from './common/adapters/socket.adapter'; +import { LoggingInterceptor } from './common/interceptors/logging.interceptor'; +import type { ConfigKeyPaths } from './config'; +import { isDev, isMainProcess } from './global/env'; +import { setupSwagger } from './setup-swagger'; +import { LoggerService } from './shared/logger/logger.service'; + +declare const module: any; + +async function bootstrap() { + const app = await NestFactory.create(AppModule, fastifyApp, { + bufferLogs: true, + snapshot: true + // forceCloseConnections: true, + }); + + const configService = app.get(ConfigService); + + const { port, globalPrefix } = configService.get('app', { infer: true }); + + // class-validator 的 DTO 类中注入 nest 容器的依赖 (用于自定义验证器) + useContainer(app.select(AppModule), { fallbackOnErrors: true }); + app.enableCors({ origin: '*', credentials: true }); + app.setGlobalPrefix(globalPrefix); + app.useStaticAssets({ root: path.join(__dirname, '..', 'public') }); + // Starts listening for shutdown hooks + !isDev && app.enableShutdownHooks(); + + if (isDev) app.useGlobalInterceptors(new LoggingInterceptor()); + + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + whitelist: true, + transformOptions: { enableImplicitConversion: true }, + // forbidNonWhitelisted: true, // 禁止 无装饰器验证的数据通过 + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + stopAtFirstError: true, + exceptionFactory: errors => + new UnprocessableEntityException( + errors.map(e => { + const rule = Object.keys(e.constraints!)[0]; + const msg = e.constraints![rule]; + return msg; + })[0] + ) + }) + ); + + app.useWebSocketAdapter(new RedisIoAdapter(app)); + + setupSwagger(app, configService); + + await app.listen(port, '0.0.0.0', async () => { + app.useLogger(app.get(LoggerService)); + const url = await app.getUrl(); + const { pid } = process; + const env = cluster.isPrimary; + const prefix = env ? 'P' : 'W'; + + if (!isMainProcess) return; + + const logger = new Logger('NestApplication'); + logger.log(`[${prefix + pid}] Server running on ${url}`); + + if (isDev) logger.log(`[${prefix + pid}] OpenAPI: ${url}/api-docs`); + }); + + if (module.hot) { + module.hot.accept(); + module.hot.dispose(() => app.close()); + } +} + +bootstrap(); diff --git a/src/migrations/1707996695540-initData.ts b/src/migrations/1707996695540-initData.ts new file mode 100644 index 0000000..8434099 --- /dev/null +++ b/src/migrations/1707996695540-initData.ts @@ -0,0 +1,14 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { MigrationInterface, QueryRunner } from 'typeorm'; + +const sql = fs.readFileSync(path.join(__dirname, '../../init_data/sql/hxoa.sql'), 'utf8'); + +export class InitData1707996695540 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(sql); + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/src/modules/auth/auth.constant.ts b/src/modules/auth/auth.constant.ts new file mode 100644 index 0000000..0815718 --- /dev/null +++ b/src/modules/auth/auth.constant.ts @@ -0,0 +1,26 @@ +export const PUBLIC_KEY = '__public_key__'; + +export const PERMISSION_KEY = '__permission_key__'; + +export const RESOURCE_KEY = '__resource_key__'; + +export const ALLOW_ANON_KEY = '__allow_anon_permission_key__'; + +export const AuthStrategy = { + LOCAL: 'local', + LOCAL_EMAIL: 'local_email', + LOCAL_PHONE: 'local_phone', + + JWT: 'jwt', + + GITHUB: 'github', + GOOGLE: 'google' +} as const; + +export const Roles = { + ADMIN: 'admin', + USER: 'user' + // GUEST: 'guest', +} as const; + +export type Role = (typeof Roles)[keyof typeof Roles]; diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..66b5b59 --- /dev/null +++ b/src/modules/auth/auth.controller.ts @@ -0,0 +1,60 @@ +import { Body, Controller, Headers, Post, UseGuards } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { Ip, IsMobile } from '~/common/decorators/http.decorator'; + +import { UserService } from '../user/user.service'; + +import { AuthService } from './auth.service'; +import { Public } from './decorators/public.decorator'; +import { LoginDto, RegisterDto } from './dto/auth.dto'; +import { LocalGuard } from './guards/local.guard'; +import { LoginToken } from './models/auth.model'; +import { CaptchaService } from './services/captcha.service'; +import { AuthUser } from './decorators/auth-user.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { Domain, SkDomain } from '~/common/decorators/domain.decorator'; + +@ApiTags('Auth - 认证模块') +@UseGuards(LocalGuard) +@Public() +@Controller('auth') +export class AuthController { + constructor( + private authService: AuthService, + private userService: UserService, + private captchaService: CaptchaService + ) {} + + @Post('login') + @ApiOperation({ summary: '登录' }) + @ApiResult({ type: LoginToken }) + async login( + @Body() dto: LoginDto, + @Ip() ip: string, + @IsMobile() isMobile: boolean, + @Headers('user-agent') ua: string + ): Promise { + if (!isMobile) { + await this.captchaService.checkImgCaptcha(dto.captchaId, dto.verifyCode); + } + const token = await this.authService.login(dto.username, dto.password, ip, ua); + return { token }; + } + + @Post('unlock') + @ApiSecurityAuth() + @ApiOperation({ summary: '屏幕解锁,使用密码和token' }) + @ApiResult({ type: LoginToken }) + async unlock(@Body() dto: LoginDto, @AuthUser() user: IAuthUser): Promise { + await this.authService.unlock(user.uid, dto.password); + return true; + } + + @Post('register') + @ApiOperation({ summary: '注册' }) + async register(@Domain() domain: SkDomain, @Body() dto: RegisterDto): Promise { + await this.userService.register(dto, domain); + } +} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..e0c54d6 --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -0,0 +1,58 @@ +import { Module } from '@nestjs/common'; + +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { ConfigKeyPaths, ISecurityConfig } from '~/config'; +import { isDev } from '~/global/env'; + +import { LogModule } from '../system/log/log.module'; +import { MenuModule } from '../system/menu/menu.module'; +import { RoleModule } from '../system/role/role.module'; +import { UserModule } from '../user/user.module'; + +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { AccountController } from './controllers/account.controller'; +import { CaptchaController } from './controllers/captcha.controller'; +import { EmailController } from './controllers/email.controller'; +import { AccessTokenEntity } from './entities/access-token.entity'; +import { RefreshTokenEntity } from './entities/refresh-token.entity'; +import { CaptchaService } from './services/captcha.service'; +import { TokenService } from './services/token.service'; +import { JwtStrategy } from './strategies/jwt.strategy'; +import { LocalStrategy } from './strategies/local.strategy'; + +const controllers = [AuthController, AccountController, CaptchaController, EmailController]; +const providers = [AuthService, TokenService, CaptchaService]; +const strategies = [LocalStrategy, JwtStrategy]; + +@Module({ + imports: [ + TypeOrmModule.forFeature([AccessTokenEntity, RefreshTokenEntity]), + PassportModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => { + const { jwtSecret, jwtExprire } = configService.get('security'); + + return { + secret: jwtSecret, + expires: jwtExprire, + ignoreExpiration: isDev + }; + }, + inject: [ConfigService] + }), + UserModule, + RoleModule, + MenuModule, + LogModule + ], + controllers: [...controllers], + providers: [...providers, ...strategies], + exports: [TypeOrmModule, JwtModule, ...providers] +}) +export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..ab152f8 --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -0,0 +1,162 @@ +import { InjectRedis } from '@liaoliaots/nestjs-redis'; +import { Injectable } from '@nestjs/common'; + +import Redis from 'ioredis'; +import { isEmpty } from 'lodash'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; + +import { ErrorEnum } from '~/constants/error-code.constant'; +import { genAuthPVKey, genAuthPermKey, genAuthTokenKey } from '~/helper/genRedisKey'; + +import { UserService } from '~/modules/user/user.service'; + +import { md5 } from '~/utils'; + +import { LoginLogService } from '../system/log/services/login-log.service'; +import { MenuService } from '../system/menu/menu.service'; +import { RoleService } from '../system/role/role.service'; + +import { TokenService } from './services/token.service'; + +@Injectable() +export class AuthService { + constructor( + @InjectRedis() private readonly redis: Redis, + private menuService: MenuService, + private roleService: RoleService, + private userService: UserService, + private loginLogService: LoginLogService, + private tokenService: TokenService + ) {} + + async validateUser(credential: string, password: string): Promise { + const user = await this.userService.findUserByUserName(credential); + + if (isEmpty(user)) throw new BusinessException(ErrorEnum.USER_NOT_FOUND); + + const comparePassword = md5(`${password}${user.psalt}`); + if (user.password !== comparePassword) + throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD); + + if (user) { + const { password, ...result } = user; + return result; + } + + return null; + } + + /** + * 获取登录JWT + * 返回null则账号密码有误,不存在该用户 + */ + async login(username: string, password: string, ip: string, ua: string): Promise { + const user = await this.userService.findUserByUserName(username); + if (isEmpty(user)) throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD); + + const comparePassword = md5(`${password}${user.psalt}`); + if (user.password !== comparePassword) + throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD); + + const roleIds = await this.roleService.getRoleIdsByUser(user.id); + + const roles = await this.roleService.getRoleValues(roleIds); + + // 包含access_token和refresh_token + const token = await this.tokenService.generateAccessToken(user.id, roles); + + await this.redis.set(genAuthTokenKey(user.id), token.accessToken); + + // 设置密码版本号 当密码修改时,版本号+1 + await this.redis.set(genAuthPVKey(user.id), 1); + + // 设置菜单权限 + const permissions = await this.menuService.getPermissions(user.id); + await this.setPermissionsCache(user.id, permissions); + + await this.loginLogService.create(user.id, ip, ua); + + return token.accessToken; + } + + /** + * 解锁屏幕 + * 返回null则账号密码有误,不存在该用户 + */ + async unlock(uid: number, password: string): Promise { + const user = await this.userService.findUserById(uid); + if (isEmpty(user)) throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD); + + const comparePassword = md5(`${password}${user.psalt}`); + if (user.password !== comparePassword) + throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD); + } + + /** + * 效验账号密码 + */ + async checkPassword(username: string, password: string) { + const user = await this.userService.findUserByUserName(username); + + const comparePassword = md5(`${password}${user.psalt}`); + if (user.password !== comparePassword) + throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD); + } + + async loginLog(uid: number, ip: string, ua: string) { + await this.loginLogService.create(uid, ip, ua); + } + + async logout(uid: number) { + // 删除token + await this.userService.forbidden(uid); + } + + /** + * 重置密码 + */ + async resetPassword(username: string, password: string) { + const user = await this.userService.findUserByUserName(username); + + await this.userService.forceUpdatePassword(user.id, password); + } + + /** + * 清除登录状态信息 + */ + async clearLoginStatus(uid: number): Promise { + await this.userService.forbidden(uid); + } + + /** + * 获取菜单列表 + */ + async getMenus(uid: number, isApp: number): Promise { + return this.menuService.getMenus(uid,isApp); + } + + /** + * 获取权限列表 + */ + async getPermissions(uid: number): Promise { + return this.menuService.getPermissions(uid); + } + + async getPermissionsCache(uid: number): Promise { + const permissionString = await this.redis.get(genAuthPermKey(uid)); + return permissionString ? JSON.parse(permissionString) : []; + } + + async setPermissionsCache(uid: number, permissions: string[]): Promise { + await this.redis.set(genAuthPermKey(uid), JSON.stringify(permissions)); + } + + async getPasswordVersionByUid(uid: number): Promise { + return this.redis.get(genAuthPVKey(uid)); + } + + async getTokenByUid(uid: number): Promise { + return this.redis.get(genAuthTokenKey(uid)); + } +} diff --git a/src/modules/auth/controllers/account.controller.ts b/src/modules/auth/controllers/account.controller.ts new file mode 100644 index 0000000..65c0661 --- /dev/null +++ b/src/modules/auth/controllers/account.controller.ts @@ -0,0 +1,79 @@ +import { Body, Controller, Get, Post, Put, UseGuards } from '@nestjs/common'; +import { ApiExtraModels, ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; + +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { AllowAnon } from '~/modules/auth/decorators/allow-anon.decorator'; +import { AuthUser } from '~/modules/auth/decorators/auth-user.decorator'; + +import { PasswordUpdateDto } from '~/modules/user/dto/password.dto'; + +import { AccountInfo } from '../../user/user.model'; +import { UserService } from '../../user/user.service'; +import { AuthService } from '../auth.service'; +import { AccountMenus, AccountUpdateDto } from '../dto/account.dto'; +import { JwtAuthGuard } from '../guards/jwt-auth.guard'; +import { IsMobile } from '~/common/decorators/http.decorator'; +import { ResourceDeviceEnum } from '~/constants/enum'; +import { Domain } from '~/common/decorators/domain.decorator'; + +@ApiTags('Account - 账户模块') +@ApiSecurityAuth() +@ApiExtraModels(AccountInfo) +@UseGuards(JwtAuthGuard) +@Controller('account') +export class AccountController { + constructor( + private userService: UserService, + private authService: AuthService + ) {} + + @Get('profile') + @ApiOperation({ summary: '获取账户资料' }) + @ApiResult({ type: AccountInfo }) + @AllowAnon() + async profile(@AuthUser() user: IAuthUser): Promise { + return this.userService.getAccountInfo(user.uid); + } + + @Get('logout') + @ApiOperation({ summary: '账户登出' }) + @AllowAnon() + async logout(@AuthUser() user: IAuthUser): Promise { + await this.authService.clearLoginStatus(user.uid); + } + + @Get('menus') + @ApiOperation({ summary: '获取菜单列表' }) + @ApiResult({ type: [AccountMenus] }) + @AllowAnon() + async menu(@AuthUser() user: IAuthUser, @IsMobile() isApp: boolean): Promise { + return this.authService.getMenus( + user.uid, + isApp ? ResourceDeviceEnum.APP : ResourceDeviceEnum.PC + ); + } + + @Get('permissions') + @ApiOperation({ summary: '获取权限列表' }) + @ApiResult({ type: [String] }) + @AllowAnon() + async permissions(@AuthUser() user: IAuthUser): Promise { + return this.authService.getPermissions(user.uid); + } + + @Put('update') + @ApiOperation({ summary: '更改账户资料' }) + @AllowAnon() + async update(@AuthUser() user: IAuthUser, @Body() dto: AccountUpdateDto): Promise { + await this.userService.updateAccountInfo(user.uid, dto); + } + + @Post('password') + @ApiOperation({ summary: '更改账户密码' }) + @AllowAnon() + async password(@AuthUser() user: IAuthUser, @Body() dto: PasswordUpdateDto): Promise { + await this.userService.updatePassword(user.uid, dto); + } +} diff --git a/src/modules/auth/controllers/captcha.controller.ts b/src/modules/auth/controllers/captcha.controller.ts new file mode 100644 index 0000000..41b0591 --- /dev/null +++ b/src/modules/auth/controllers/captcha.controller.ts @@ -0,0 +1,48 @@ +import { InjectRedis } from '@liaoliaots/nestjs-redis'; +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import Redis from 'ioredis'; +import { isEmpty } from 'lodash'; +import * as svgCaptcha from 'svg-captcha'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { genCaptchaImgKey } from '~/helper/genRedisKey'; +import { generateUUID } from '~/utils'; + +import { Public } from '../decorators/public.decorator'; + +import { ImageCaptchaDto } from '../dto/captcha.dto'; +import { ImageCaptcha } from '../models/auth.model'; + +@ApiTags('Captcha - 验证码模块') +// @UseGuards(ThrottlerGuard) +@Controller('auth/captcha') +export class CaptchaController { + constructor(@InjectRedis() private redis: Redis) {} + + @Get('img') + @ApiOperation({ summary: '获取登录图片验证码' }) + @ApiResult({ type: ImageCaptcha }) + @Public() + // @Throttle({ default: { limit: 2, ttl: 600000 } }) + async captchaByImg(@Query() dto: ImageCaptchaDto): Promise { + const { width, height } = dto; + + const svg = svgCaptcha.create({ + size: 4, + color: true, + noise: 4, + width: isEmpty(width) ? 100 : width, + height: isEmpty(height) ? 50 : height, + charPreset: '1234567890' + }); + const result = { + img: `data:image/svg+xml;base64,${Buffer.from(svg.data).toString('base64')}`, + id: generateUUID() + }; + // 5分钟过期时间 + await this.redis.set(genCaptchaImgKey(result.id), svg.text, 'EX', 60 * 5); + return result; + } +} diff --git a/src/modules/auth/controllers/email.controller.ts b/src/modules/auth/controllers/email.controller.ts new file mode 100644 index 0000000..f0fb6c4 --- /dev/null +++ b/src/modules/auth/controllers/email.controller.ts @@ -0,0 +1,38 @@ +import { Body, Controller, Post, UseGuards } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { Throttle, ThrottlerGuard } from '@nestjs/throttler'; + +import { Ip } from '~/common/decorators/http.decorator'; + +import { MailerService } from '~/shared/mailer/mailer.service'; + +import { Public } from '../decorators/public.decorator'; + +import { SendEmailCodeDto } from '../dto/captcha.dto'; + +@ApiTags('Auth - 认证模块') +@UseGuards(ThrottlerGuard) +@Controller('auth/email') +export class EmailController { + constructor(private mailerService: MailerService) {} + + @Post('send') + @ApiOperation({ summary: '发送邮箱验证码' }) + @Public() + @Throttle({ default: { limit: 2, ttl: 600000 } }) + async sendEmailCode(@Body() dto: SendEmailCodeDto, @Ip() ip: string): Promise { + // await this.authService.checkImgCaptcha(dto.captchaId, dto.verifyCode); + const { email } = dto; + + await this.mailerService.checkLimit(email, ip); + const { code } = await this.mailerService.sendVerificationCode(email); + + await this.mailerService.log(email, code, ip); + } + + // @Post() + // async authWithEmail(@AuthUser() user: IAuthUser) { + // // TODO: + // } +} diff --git a/src/modules/auth/decorators/allow-anon.decorator.ts b/src/modules/auth/decorators/allow-anon.decorator.ts new file mode 100644 index 0000000..b70dfac --- /dev/null +++ b/src/modules/auth/decorators/allow-anon.decorator.ts @@ -0,0 +1,8 @@ +import { SetMetadata } from '@nestjs/common'; + +import { ALLOW_ANON_KEY } from '../auth.constant'; + +/** + * 当接口不需要检测用户是否具有操作权限时添加该装饰器 + */ +export const AllowAnon = () => SetMetadata(ALLOW_ANON_KEY, true); diff --git a/src/modules/auth/decorators/auth-user.decorator.ts b/src/modules/auth/decorators/auth-user.decorator.ts new file mode 100644 index 0000000..bac65da --- /dev/null +++ b/src/modules/auth/decorators/auth-user.decorator.ts @@ -0,0 +1,15 @@ +import { ExecutionContext, createParamDecorator } from '@nestjs/common'; +import { FastifyRequest } from 'fastify'; + +type Payload = keyof IAuthUser; + +/** + * @description 获取当前登录用户信息, 并挂载到request上 + */ +export const AuthUser = createParamDecorator((data: Payload, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + // auth guard will mount this + const user = request.user as IAuthUser; + + return data ? user?.[data] : user; +}); diff --git a/src/modules/auth/decorators/permission.decorator.ts b/src/modules/auth/decorators/permission.decorator.ts new file mode 100644 index 0000000..e88096c --- /dev/null +++ b/src/modules/auth/decorators/permission.decorator.ts @@ -0,0 +1,63 @@ +import { SetMetadata, applyDecorators } from '@nestjs/common'; + +import { isPlainObject } from 'lodash'; + +import { PERMISSION_KEY } from '../auth.constant'; + +type TupleToObject> = { + [K in Uppercase]: `${T}:${Lowercase}`; +}; +type AddPrefixToObjectValue> = { + [K in keyof P]: K extends string ? `${T}:${P[K]}` : never; +}; + +/** 资源操作需要特定的权限 */ +export function Perm(permission: string | string[]) { + return applyDecorators(SetMetadata(PERMISSION_KEY, permission)); +} + +/** (此举非必需)保存通过 definePermission 定义的所有权限,可用于前端开发人员开发阶段的 ts 类型提示,避免前端权限定义与后端定义不匹配 */ +let permissions: string[] = []; +/** + * 定义权限,同时收集所有被定义的权限 + * + * - 通过对象形式定义, eg: + * ```ts + * definePermission('app:health', { + * NETWORK: 'network' + * }; + * ``` + * + * - 通过字符串数组形式定义, eg: + * ```ts + * definePermission('app:health', ['network']); + * ``` + */ +export function definePermission>( + modulePrefix: T, + actionMap: U +): AddPrefixToObjectValue; +export function definePermission>( + modulePrefix: T, + actions: U +): TupleToObject; +export function definePermission(modulePrefix: string, actions) { + if (isPlainObject(actions)) { + Object.entries(actions).forEach(([key, action]) => { + actions[key] = `${modulePrefix}:${action}`; + }); + permissions = [...new Set([...permissions, ...Object.values(actions)])]; + return actions; + } else if (Array.isArray(actions)) { + const permissionFormats = actions.map(action => `${modulePrefix}:${action}`); + permissions = [...new Set([...permissions, ...permissionFormats])]; + + return actions.reduce((prev, action) => { + prev[action.toUpperCase()] = `${modulePrefix}:${action}`; + return prev; + }, {}); + } +} + +/** 获取所有通过 definePermission 定义的权限 */ +export const getDefinePermissions = () => permissions; diff --git a/src/modules/auth/decorators/public.decorator.ts b/src/modules/auth/decorators/public.decorator.ts new file mode 100644 index 0000000..b9592f2 --- /dev/null +++ b/src/modules/auth/decorators/public.decorator.ts @@ -0,0 +1,8 @@ +import { SetMetadata } from '@nestjs/common'; + +import { PUBLIC_KEY } from '../auth.constant'; + +/** + * 当接口不需要检测用户登录时添加该装饰器 + */ +export const Public = () => SetMetadata(PUBLIC_KEY, true); diff --git a/src/modules/auth/decorators/resource.decorator.ts b/src/modules/auth/decorators/resource.decorator.ts new file mode 100644 index 0000000..6734598 --- /dev/null +++ b/src/modules/auth/decorators/resource.decorator.ts @@ -0,0 +1,22 @@ +import { SetMetadata, applyDecorators } from '@nestjs/common'; + +import { ObjectLiteral, ObjectType, Repository } from 'typeorm'; + +import { RESOURCE_KEY } from '../auth.constant'; + +export type Condition = ( + Repository: Repository, + items: number[], + user: IAuthUser +) => Promise; + +export interface ResourceObject { + entity: ObjectType; + condition: Condition; +} +export function Resource( + entity: ObjectType, + condition?: Condition +) { + return applyDecorators(SetMetadata(RESOURCE_KEY, { entity, condition })); +} diff --git a/src/modules/auth/dto/account.dto.ts b/src/modules/auth/dto/account.dto.ts new file mode 100644 index 0000000..cf87459 --- /dev/null +++ b/src/modules/auth/dto/account.dto.ts @@ -0,0 +1,72 @@ +import { ApiProperty, OmitType, PartialType, PickType } from '@nestjs/swagger'; +import { IsEmail, IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator'; + +import { MenuEntity } from '~/modules/system/menu/menu.entity'; + +export class AccountUpdateDto { + @ApiProperty({ description: '用户呢称' }) + @IsString() + @IsOptional() + nickname: string; + + @ApiProperty({ description: '用户邮箱' }) + @IsEmail() + email: string; + + @ApiProperty({ description: '用户QQ' }) + @IsOptional() + @IsString() + @Matches(/^[0-9]+$/) + @MinLength(5) + @MaxLength(11) + qq: string; + + @ApiProperty({ description: '用户手机号' }) + @IsOptional() + @IsString() + phone: string; + + @ApiProperty({ description: '用户头像' }) + @IsOptional() + @IsString() + avatar: string; + + @ApiProperty({ description: '用户备注' }) + @IsOptional() + @IsString() + remark: string; +} + +export class ResetPasswordDto { + @ApiProperty({ description: '临时token', example: 'uuid' }) + @IsString() + accessToken: string; + + @ApiProperty({ description: '密码', example: 'a123456' }) + @IsString() + @Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/) + @MinLength(6) + password: string; +} + +export class MenuMeta extends PartialType( + OmitType(MenuEntity, [ + 'parentId', + 'createdAt', + 'updatedAt', + 'id', + 'roles', + 'path', + 'name' + ] as const) +) { + title: string; +} +export class AccountMenus extends PickType(MenuEntity, [ + 'id', + 'path', + 'name', + 'component' +] as const) { + meta: MenuMeta; +} diff --git a/src/modules/auth/dto/auth.dto.ts b/src/modules/auth/dto/auth.dto.ts new file mode 100644 index 0000000..f6ec215 --- /dev/null +++ b/src/modules/auth/dto/auth.dto.ts @@ -0,0 +1,42 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator'; + +export class LoginDto { + @ApiProperty({ description: '手机号/邮箱' }) + @IsOptional() + username: string; + + @ApiProperty({ description: '密码', example: 'a123456' }) + @IsString() + @Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/, { message: '密码错误' }) + @MinLength(6) + password: string; + + @ApiProperty({ description: '验证码标识,手机端不需要' }) + @IsOptional() + captchaId: string; + + @ApiProperty({ description: '用户输入的验证码' }) + @IsOptional() + @MinLength(4) + @MaxLength(4) + verifyCode: string; +} + +export class RegisterDto { + @ApiProperty({ description: '账号' }) + @IsString() + username: string; + + @ApiProperty({ description: '密码' }) + @IsString() + @Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/) + @MinLength(6) + @MaxLength(16) + password: string; + + @ApiProperty({ description: '语言', examples: ['EN', 'ZH'] }) + @IsString() + lang: string; +} diff --git a/src/modules/auth/dto/captcha.dto.ts b/src/modules/auth/dto/captcha.dto.ts new file mode 100644 index 0000000..fdff06c --- /dev/null +++ b/src/modules/auth/dto/captcha.dto.ts @@ -0,0 +1,47 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsEmail, IsInt, IsMobilePhone, IsOptional, IsString } from 'class-validator'; + +export class ImageCaptchaDto { + @ApiProperty({ + required: false, + default: 100, + description: '验证码宽度' + }) + @Type(() => Number) + @IsInt() + @IsOptional() + readonly width: number = 100; + + @ApiProperty({ + required: false, + default: 50, + description: '验证码宽度' + }) + @Type(() => Number) + @IsInt() + @IsOptional() + readonly height: number = 50; +} + +export class SendEmailCodeDto { + @ApiProperty({ description: '邮箱' }) + @IsEmail({}, { message: '邮箱格式不正确' }) + email: string; +} + +export class SendSmsCodeDto { + @ApiProperty({ description: '手机号' }) + @IsMobilePhone('zh-CN', {}, { message: '手机号格式不正确' }) + phone: string; +} + +export class CheckCodeDto { + @ApiProperty({ description: '手机号/邮箱' }) + @IsString() + account: string; + + @ApiProperty({ description: '验证码' }) + @IsString() + code: string; +} diff --git a/src/modules/auth/entities/access-token.entity.ts b/src/modules/auth/entities/access-token.entity.ts new file mode 100644 index 0000000..fbbdc0c --- /dev/null +++ b/src/modules/auth/entities/access-token.entity.ts @@ -0,0 +1,40 @@ +import { + BaseEntity, + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToOne, + PrimaryGeneratedColumn +} from 'typeorm'; + +import { UserEntity } from '~/modules/user/user.entity'; + +import { RefreshTokenEntity } from './refresh-token.entity'; + +@Entity('user_access_tokens') +export class AccessTokenEntity extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ length: 500 }) + value!: string; + + @Column({ comment: '令牌过期时间' }) + expired_at!: Date; + + @CreateDateColumn({ comment: '令牌创建时间' }) + created_at!: Date; + + @OneToOne(() => RefreshTokenEntity, refreshToken => refreshToken.accessToken, { + cascade: true + }) + refreshToken!: RefreshTokenEntity; + + @ManyToOne(() => UserEntity, user => user.accessTokens, { + onDelete: 'CASCADE' + }) + @JoinColumn({ name: 'user_id' }) + user!: UserEntity; +} diff --git a/src/modules/auth/entities/refresh-token.entity.ts b/src/modules/auth/entities/refresh-token.entity.ts new file mode 100644 index 0000000..4df36ce --- /dev/null +++ b/src/modules/auth/entities/refresh-token.entity.ts @@ -0,0 +1,32 @@ +import { + BaseEntity, + Column, + CreateDateColumn, + Entity, + JoinColumn, + OneToOne, + PrimaryGeneratedColumn +} from 'typeorm'; + +import { AccessTokenEntity } from './access-token.entity'; + +@Entity('user_refresh_tokens') +export class RefreshTokenEntity extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ length: 500 }) + value!: string; + + @Column({ comment: '令牌过期时间' }) + expired_at!: Date; + + @CreateDateColumn({ comment: '令牌创建时间' }) + created_at!: Date; + + @OneToOne(() => AccessTokenEntity, accessToken => accessToken.refreshToken, { + onDelete: 'CASCADE' + }) + @JoinColumn() + accessToken!: AccessTokenEntity; +} diff --git a/src/modules/auth/guards/jwt-auth.guard.ts b/src/modules/auth/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..4349a13 --- /dev/null +++ b/src/modules/auth/guards/jwt-auth.guard.ts @@ -0,0 +1,94 @@ +import { ExecutionContext, HttpException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; +import { FastifyRequest } from 'fastify'; +import { isEmpty, isNil } from 'lodash'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { AuthService } from '~/modules/auth/auth.service'; + +import { checkIsDemoMode } from '~/utils'; + +import { AuthStrategy, PUBLIC_KEY } from '../auth.constant'; +import { TokenService } from '../services/token.service'; + +// https://docs.nestjs.com/recipes/passport#implement-protected-route-and-jwt-strategy-guards +@Injectable() +export class JwtAuthGuard extends AuthGuard(AuthStrategy.JWT) { + constructor( + private reflector: Reflector, + private authService: AuthService, + private tokenService: TokenService + ) { + super(); + } + + async canActivate(context: ExecutionContext): Promise { + const isPublic = this.reflector.getAllAndOverride(PUBLIC_KEY, [ + context.getHandler(), + context.getClass() + ]); + const request = context.switchToHttp().getRequest(); + // const response = context.switchToHttp().getResponse() + + // TODO 此处代码的作用是判断如果在演示环境下,则拒绝用户的增删改操作,去掉此代码不影响正常的业务逻辑 + if (request.method !== 'GET' && !request.url.includes('/auth/login')) checkIsDemoMode(); + + const isSse = request.headers.accept === 'text/event-stream'; + + if (isSse && !request.headers.authorization?.startsWith('Bearer')) { + const { token } = request.query as Record; + if (token) request.headers.authorization = `Bearer ${token}`; + } + + const Authorization = request.headers.authorization; + + let result: any = false; + try { + result = await super.canActivate(context); + } catch (e) { + // 需要后置判断 这样携带了 token 的用户就能够解析到 request.user + if (isPublic) return true; + + if (isEmpty(Authorization)) throw new UnauthorizedException('未登录'); + + // 判断 token 是否存在, 如果不存在则认证失败 + const accessToken = isNil(Authorization) + ? undefined + : await this.tokenService.checkAccessToken(Authorization!); + + if (!accessToken) throw new UnauthorizedException('令牌无效'); + } + + // SSE 请求 + if (isSse) { + const { uid } = request.params as Record; + + if (Number(uid) !== request.user.uid) + throw new UnauthorizedException('路径参数 uid 与当前 token 登录的用户 uid 不一致'); + } + + const pv = await this.authService.getPasswordVersionByUid(request.user.uid); + if (pv !== `${request.user.pv}`) { + // 密码版本不一致,登录期间已更改过密码 + throw new HttpException(ErrorEnum.INVALID_LOGIN,HttpStatus.UNAUTHORIZED); + } + + // 不允许多端登录 + // const cacheToken = await this.authService.getTokenByUid(request.user.uid); + // if (Authorization !== cacheToken) { + // // 与redis保存不一致 即二次登录 + // throw new ApiException(ErrorEnum.CODE_1106); + // } + + return result; + } + + handleRequest(err, user, info) { + // You can throw an exception based on either "info" or "err" arguments + if (err || !user) throw err || new UnauthorizedException(); + + return user; + } +} diff --git a/src/modules/auth/guards/local.guard.ts b/src/modules/auth/guards/local.guard.ts new file mode 100644 index 0000000..2bcaca9 --- /dev/null +++ b/src/modules/auth/guards/local.guard.ts @@ -0,0 +1,11 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +import { AuthStrategy } from '../auth.constant'; + +@Injectable() +export class LocalGuard extends AuthGuard(AuthStrategy.LOCAL) { + async canActivate(context: ExecutionContext) { + return true; + } +} diff --git a/src/modules/auth/guards/rbac.guard.ts b/src/modules/auth/guards/rbac.guard.ts new file mode 100644 index 0000000..3cf7239 --- /dev/null +++ b/src/modules/auth/guards/rbac.guard.ts @@ -0,0 +1,64 @@ +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { FastifyRequest } from 'fastify'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { AuthService } from '~/modules/auth/auth.service'; + +import { ALLOW_ANON_KEY, PERMISSION_KEY, PUBLIC_KEY, Roles } from '../auth.constant'; + +@Injectable() +export class RbacGuard implements CanActivate { + constructor( + private reflector: Reflector, + private authService: AuthService + ) {} + + async canActivate(context: ExecutionContext): Promise { + const isPublic = this.reflector.getAllAndOverride(PUBLIC_KEY, [ + context.getHandler(), + context.getClass() + ]); + + if (isPublic) return true; + + const request = context.switchToHttp().getRequest(); + + const { user } = request; + if (!user) throw new UnauthorizedException('登录无效'); + + // allowAnon 是需要登录后可访问(无需权限), Public 则是无需登录也可访问. + const allowAnon = this.reflector.get(ALLOW_ANON_KEY, context.getHandler()); + if (allowAnon) return true; + + const payloadPermission = this.reflector.getAllAndOverride(PERMISSION_KEY, [ + context.getHandler(), + context.getClass() + ]); + + // 控制器没有设置接口权限,则默认通过 + if (!payloadPermission) return true; + + // 管理员放开所有权限 + if (user.roles.includes(Roles.ADMIN)) return true; + + const allPermissions = + (await this.authService.getPermissionsCache(user.uid)) ?? + (await this.authService.getPermissions(user.uid)); + // console.log(allPermissions) + let canNext = false; + + // handle permission strings + if (Array.isArray(payloadPermission)) { + // 只要有一个权限满足即可 + canNext = payloadPermission.every(i => allPermissions.includes(i)); + } + + if (typeof payloadPermission === 'string') canNext = allPermissions.includes(payloadPermission); + + if (!canNext) throw new BusinessException(ErrorEnum.NO_PERMISSION); + + return true; + } +} diff --git a/src/modules/auth/guards/resource.guard.ts b/src/modules/auth/guards/resource.guard.ts new file mode 100644 index 0000000..301181f --- /dev/null +++ b/src/modules/auth/guards/resource.guard.ts @@ -0,0 +1,81 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { FastifyRequest } from 'fastify'; + +import { isArray, isEmpty, isNil } from 'lodash'; + +import { DataSource, In, Repository } from 'typeorm'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; + +import { ErrorEnum } from '~/constants/error-code.constant'; + +import { PUBLIC_KEY, RESOURCE_KEY, Roles } from '../auth.constant'; +import { ResourceObject } from '../decorators/resource.decorator'; + +@Injectable() +export class ResourceGuard implements CanActivate { + constructor( + private reflector: Reflector, + private dataSource: DataSource + ) {} + + async canActivate(context: ExecutionContext): Promise { + const isPublic = this.reflector.getAllAndOverride(PUBLIC_KEY, [ + context.getHandler(), + context.getClass() + ]); + + const request = context.switchToHttp().getRequest(); + const isSse = request.headers.accept === 'text/event-stream'; + // 忽略 sse 请求 + if (isPublic || isSse) return true; + + const { user } = request; + + if (!user) return false; + + // 如果是检查资源所属,且不是超级管理员,还需要进一步判断是否是自己的数据 + const { entity, condition } = this.reflector.get( + RESOURCE_KEY, + context.getHandler() + ) ?? { entity: null, condition: null }; + + if (entity && !user.roles.includes(Roles.ADMIN)) { + const repo: Repository = this.dataSource.getRepository(entity); + + /** + * 获取请求中的 items (ids) 验证数据拥有者 + * @param request + */ + const getRequestItems = (request?: FastifyRequest): number[] => { + const { params = {}, body = {}, query = {} } = (request ?? {}) as any; + const id = params.id ?? body.id ?? query.id; + + if (id) return [id]; + + const { items } = body; + return !isNil(items) && isArray(items) ? items : []; + }; + + const items = getRequestItems(request); + if (isEmpty(items)) throw new BusinessException(ErrorEnum.REQUESTED_RESOURCE_NOT_FOUND); + + if (condition) return condition(repo, items, user); + + const recordQuery = { + where: { + id: In(items), + user: { id: user.uid } + }, + relations: ['user'] + }; + + const records = await repo.find(recordQuery); + + if (isEmpty(records)) throw new BusinessException(ErrorEnum.REQUESTED_RESOURCE_NOT_FOUND); + } + + return true; + } +} diff --git a/src/modules/auth/models/auth.model.ts b/src/modules/auth/models/auth.model.ts new file mode 100644 index 0000000..01a5e08 --- /dev/null +++ b/src/modules/auth/models/auth.model.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ImageCaptcha { + @ApiProperty({ description: 'base64格式的svg图片' }) + img: string; + + @ApiProperty({ description: '验证码对应的唯一ID' }) + id: string; +} + +export class LoginToken { + @ApiProperty({ description: 'JWT身份Token' }) + token: string; +} diff --git a/src/modules/auth/services/captcha.service.ts b/src/modules/auth/services/captcha.service.ts new file mode 100644 index 0000000..f7ddfc0 --- /dev/null +++ b/src/modules/auth/services/captcha.service.ts @@ -0,0 +1,35 @@ +import { InjectRedis } from '@liaoliaots/nestjs-redis'; +import { Injectable } from '@nestjs/common'; + +import Redis from 'ioredis'; +import { isEmpty } from 'lodash'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { genCaptchaImgKey } from '~/helper/genRedisKey'; +import { CaptchaLogService } from '~/modules/system/log/services/captcha-log.service'; + +@Injectable() +export class CaptchaService { + constructor( + @InjectRedis() private redis: Redis, + + private captchaLogService: CaptchaLogService + ) {} + + /** + * 校验图片验证码 + */ + async checkImgCaptcha(id: string, code: string): Promise { + const result = await this.redis.get(genCaptchaImgKey(id)); + if (isEmpty(result) || code.toLowerCase() !== result.toLowerCase()) + throw new BusinessException(ErrorEnum.INVALID_VERIFICATION_CODE); + + // 校验成功后移除验证码 + await this.redis.del(genCaptchaImgKey(id)); + } + + async log(account: string, code: string, provider: 'sms' | 'email', uid?: number): Promise { + await this.captchaLogService.create(account, code, provider, uid); + } +} diff --git a/src/modules/auth/services/token.service.ts b/src/modules/auth/services/token.service.ts new file mode 100644 index 0000000..2e552cd --- /dev/null +++ b/src/modules/auth/services/token.service.ts @@ -0,0 +1,150 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import dayjs from 'dayjs'; + +import { ISecurityConfig, SecurityConfig } from '~/config'; +import { RoleService } from '~/modules/system/role/role.service'; +import { UserEntity } from '~/modules/user/user.entity'; +import { generateUUID } from '~/utils'; + +import { AccessTokenEntity } from '../entities/access-token.entity'; +import { RefreshTokenEntity } from '../entities/refresh-token.entity'; + +/** + * 令牌服务 + */ +@Injectable() +export class TokenService { + constructor( + private jwtService: JwtService, + private roleService: RoleService, + @Inject(SecurityConfig.KEY) private securityConfig: ISecurityConfig + ) {} + + /** + * 根据accessToken刷新AccessToken与RefreshToken + * @param accessTokenSign + * @param response + */ + async refreshToken(accessToken: AccessTokenEntity) { + const { user, refreshToken } = accessToken; + + if (refreshToken) { + const now = dayjs(); + // 判断refreshToken是否过期 + if (now.isAfter(refreshToken.expired_at)) return null; + + const roleIds = await this.roleService.getRoleIdsByUser(user.id); + const roleValues = await this.roleService.getRoleValues(roleIds); + + // 如果没过期则生成新的access_token和refresh_token + const token = await this.generateAccessToken(user.id, roleValues); + + await accessToken.remove(); + return token; + } + return null; + } + + generateJwtSign(payload: any) { + const jwtSign = this.jwtService.sign(payload); + + return jwtSign; + } + + async generateAccessToken(uid: number, roles: string[] = []) { + const payload: IAuthUser = { + uid, + pv: 1, + roles + }; + + const jwtSign = this.jwtService.sign(payload); + + // 生成accessToken + const accessToken = new AccessTokenEntity(); + accessToken.value = jwtSign; + accessToken.user = { id: uid } as UserEntity; + accessToken.expired_at = dayjs().add(this.securityConfig.jwtExprire, 'second').toDate(); + + await accessToken.save(); + + // 生成refreshToken + const refreshToken = await this.generateRefreshToken(accessToken, dayjs()); + + return { + accessToken: jwtSign, + refreshToken + }; + } + + /** + * 生成新的RefreshToken并存入数据库 + * @param accessToken + * @param now + */ + async generateRefreshToken(accessToken: AccessTokenEntity, now: dayjs.Dayjs): Promise { + const refreshTokenPayload = { + uuid: generateUUID() + }; + + const refreshTokenSign = this.jwtService.sign(refreshTokenPayload, { + secret: this.securityConfig.refreshSecret + }); + + const refreshToken = new RefreshTokenEntity(); + refreshToken.value = refreshTokenSign; + refreshToken.expired_at = now.add(this.securityConfig.refreshExpire, 'second').toDate(); + refreshToken.accessToken = accessToken; + + await refreshToken.save(); + + return refreshTokenSign; + } + + /** + * 检查accessToken是否存在 + * @param value + */ + async checkAccessToken(value: string) { + return AccessTokenEntity.findOne({ + where: { value }, + relations: ['user', 'refreshToken'], + cache: true + }); + } + + /** + * 移除AccessToken且自动移除关联的RefreshToken + * @param value + */ + async removeAccessToken(value: string) { + const accessToken = await AccessTokenEntity.findOne({ + where: { value } + }); + if (accessToken) await accessToken.remove(); + } + + /** + * 移除RefreshToken + * @param value + */ + async removeRefreshToken(value: string) { + const refreshToken = await RefreshTokenEntity.findOne({ + where: { value }, + relations: ['accessToken'] + }); + if (refreshToken) { + if (refreshToken.accessToken) await refreshToken.accessToken.remove(); + await refreshToken.remove(); + } + } + + /** + * 验证Token是否正确,如果正确则返回所属用户对象 + * @param token + */ + async verifyAccessToken(token: string): Promise { + return this.jwtService.verify(token); + } +} diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..a5175e7 --- /dev/null +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -0,0 +1,22 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; + +import { ISecurityConfig, SecurityConfig } from '~/config'; + +import { AuthStrategy } from '../auth.constant'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy, AuthStrategy.JWT) { + constructor(@Inject(SecurityConfig.KEY) private securityConfig: ISecurityConfig) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: securityConfig.jwtSecret + }); + } + + async validate(payload: IAuthUser) { + return payload; + } +} diff --git a/src/modules/auth/strategies/local.strategy.ts b/src/modules/auth/strategies/local.strategy.ts new file mode 100644 index 0000000..da2d9d2 --- /dev/null +++ b/src/modules/auth/strategies/local.strategy.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-local'; + +import { AuthStrategy } from '../auth.constant'; +import { AuthService } from '../auth.service'; + +@Injectable() +export class LocalStrategy extends PassportStrategy(Strategy, AuthStrategy.LOCAL) { + constructor(private authService: AuthService) { + super({ + usernameField: 'credential', + passwordField: 'password' + }); + } + + async validate(username: string, password: string): Promise { + const user = await this.authService.validateUser(username, password); + return user; + } +} diff --git a/src/modules/common/base.service.ts b/src/modules/common/base.service.ts new file mode 100644 index 0000000..7cb472d --- /dev/null +++ b/src/modules/common/base.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BaseService { + generateInventoryInOutNumber(): string { + // Generate a random inventory number + return Math.floor(Math.random() * 1000000).toString(); + } + + // Add more common methods here +} diff --git a/src/modules/company/company.controller.ts b/src/modules/company/company.controller.ts new file mode 100644 index 0000000..1ad2602 --- /dev/null +++ b/src/modules/company/company.controller.ts @@ -0,0 +1,80 @@ +import { + Body, + Controller, + Get, + Query, + Put, + Delete, + Post, + BadRequestException +} from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Perm, definePermission } from '../auth/decorators/permission.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { CompanyService } from './company.service'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { CompanyEntity } from './company.entity'; +import { CompanyDto, CompanyQueryDto, CompanyUpdateDto } from './company.dto'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { Domain, SkDomain } from '~/common/decorators/domain.decorator'; +export const permissions = definePermission('app:company', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('Company - 公司') +@ApiSecurityAuth() +@Controller('company') +export class CompanyController { + constructor(private companyService: CompanyService) {} + + @Get() + @ApiOperation({ summary: '获取公司列表' }) + @ApiResult({ type: [CompanyEntity], isPage: true }) + @Perm(permissions.LIST) + async list(@Domain() domain: SkDomain, @Query() dto: CompanyQueryDto) { + return this.companyService.findAll({ ...dto, domain }); + } + + @Get(':id') + @ApiOperation({ summary: '获取公司信息' }) + @ApiResult({ type: CompanyDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.companyService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增公司' }) + @Perm(permissions.CREATE) + async create(@Domain() domain: SkDomain, @Body() dto: CompanyDto): Promise { + await this.companyService.create({ ...dto, domain }); + } + + @Put(':id') + @ApiOperation({ summary: '更新公司' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: CompanyUpdateDto): Promise { + await this.companyService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除公司' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.companyService.delete(id); + } + + @Put('unlink-attachments/:id') + @ApiOperation({ summary: '附件解除关联' }) + @Perm(permissions.UPDATE) + async unlinkAttachments( + @IdParam() id: number, + @Body() { fileIds }: CompanyUpdateDto + ): Promise { + await this.companyService.unlinkAttachments(id, fileIds); + } +} diff --git a/src/modules/company/company.dto.ts b/src/modules/company/company.dto.ts new file mode 100644 index 0000000..6a9f350 --- /dev/null +++ b/src/modules/company/company.dto.ts @@ -0,0 +1,53 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { + IsArray, + IsDate, + IsDateString, + IsIn, + IsInt, + IsNumber, + IsOptional, + IsString, + Matches, + MinLength +} from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { Storage } from '../tools/storage/storage.entity'; +import { IsUnique } from '~/shared/database/constraints/unique.constraint'; +import { CompanyEntity } from './company.entity'; +import { DomainType, SkDomain } from '~/common/decorators/domain.decorator'; + +export class CompanyDto extends DomainType { + @ApiProperty({ description: '公司名称' }) + @IsUnique(CompanyEntity, { message: '已存在同名公司' }) + @IsString() + name: string; + + @ApiProperty({ description: '附件' }) + files: Storage[]; +} + +export class CompanyUpdateDto extends PartialType(CompanyDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} + +export class ComapnyCreateDto extends PartialType(CompanyDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} + +export class CompanyQueryDto extends IntersectionType( + PagerDto, + PartialType(CompanyDto), + DomainType +) { + @ApiProperty({ description: '公司名称' }) + @IsOptional() + @IsString() + name: string; +} diff --git a/src/modules/company/company.entity.ts b/src/modules/company/company.entity.ts new file mode 100644 index 0000000..2f75103 --- /dev/null +++ b/src/modules/company/company.entity.ts @@ -0,0 +1,39 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinTable, ManyToMany, OneToMany, Relation } from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; +import { Storage } from '../tools/storage/storage.entity'; +import { ProductEntity } from '../product/product.entity'; +import { SkDomain } from '~/common/decorators/domain.decorator'; + +@Entity({ name: 'company' }) +export class CompanyEntity extends CommonEntity { + @Column({ + name: 'name', + type: 'varchar', + unique: true, + length: 255, + comment: '公司名称' + }) + @ApiProperty({ description: '公司名称' }) + name: string; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; + + @Column({ type: 'int', default: 1, comment: '所属域' }) + @ApiProperty({ description: '所属域' }) + domain: SkDomain; + + @ApiHideProperty() + @OneToMany(() => ProductEntity, product => product.company) + products: Relation; + + @ManyToMany(() => Storage, storage => storage.companys) + @JoinTable({ + name: 'company_storage', + joinColumn: { name: 'company_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } + }) + files: Relation; +} diff --git a/src/modules/company/company.module.ts b/src/modules/company/company.module.ts new file mode 100644 index 0000000..5fba5b1 --- /dev/null +++ b/src/modules/company/company.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { CompanyController } from './company.controller'; +import { CompanyService } from './company.service'; +import { CompanyEntity } from './company.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StorageModule } from '../tools/storage/storage.module'; +import { DatabaseModule } from '~/shared/database/database.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([CompanyEntity]), StorageModule, DatabaseModule], + controllers: [CompanyController], + providers: [CompanyService] +}) +export class CompanyModule {} diff --git a/src/modules/company/company.service.ts b/src/modules/company/company.service.ts new file mode 100644 index 0000000..8cb1fdd --- /dev/null +++ b/src/modules/company/company.service.ts @@ -0,0 +1,124 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { CompanyEntity } from './company.entity'; +import { EntityManager, Like, Repository } from 'typeorm'; +import { CompanyDto, CompanyQueryDto, CompanyUpdateDto } from './company.dto'; +import { Pagination } from '~/helper/paginate/pagination'; +import { paginate } from '~/helper/paginate'; +import { Storage } from '../tools/storage/storage.entity'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { fieldSearch } from '~/shared/database/field-search'; +import { SkDomain } from '~/common/decorators/domain.decorator'; + +@Injectable() +export class CompanyService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(CompanyEntity) + private companyRepository: Repository, + @InjectRepository(Storage) + private storageRepository: Repository + ) {} + + /** + * 查询所有公司 + */ + async findAll({ + page, + pageSize, + ...fields + }: CompanyQueryDto): Promise> { + const queryBuilder = this.companyRepository + .createQueryBuilder('company') + .leftJoin('company.files', 'files') + .addSelect(['files.id', 'files.path']) + .where(fieldSearch(fields)) + .andWhere('company.isDelete = 0'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create(dto: CompanyDto): Promise { + await this.companyRepository.insert(dto); + } + + /** + * 更新 + */ + async update(id: number, { fileIds, ...data }: Partial): Promise { + await this.entityManager.transaction(async manager => { + await manager.update(CompanyEntity, id, { + ...data + }); + const company = await this.companyRepository + .createQueryBuilder('company') + .leftJoinAndSelect('company.files', 'files') + .where('company.id = :id', { id }) + .getOne(); + if (fileIds?.length) { + const count = await this.storageRepository + .createQueryBuilder('storage') + .where('storage.id in(:fileIds)', { fileIds }) + .getCount(); + if (count !== fileIds?.length) { + throw new BusinessException(ErrorEnum.STORAGE_NOT_FOUND); + } + // 附件要批量插入 + await manager.createQueryBuilder().relation(CompanyEntity, 'files').of(id).add(fileIds); + } + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + // 合同比较重要,做逻辑删除 + await this.companyRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个合同信息 + */ + async info(id: number) { + const info = await this.companyRepository + .createQueryBuilder('company') + .where({ + id + }) + .andWhere('company.isDelete = 0') + .getOne(); + return info; + } + + /** + * 解除附件关联 + * @param id 合同ID + * @param fileIds 附件ID + */ + async unlinkAttachments(id: number, fileIds: number[]) { + await this.entityManager.transaction(async manager => { + const company = await this.companyRepository + .createQueryBuilder('company') + .leftJoinAndSelect('company.files', 'files') + .where('company.id = :id', { id }) + .getOne(); + const linkedFiles = company.files + .map(item => item.id) + .filter(item => !fileIds.includes(item)); + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(CompanyEntity, 'files') + .of(id) + .addAndRemove(linkedFiles, company.files); + }); + } +} diff --git a/src/modules/contract/contract.controller.ts b/src/modules/contract/contract.controller.ts new file mode 100644 index 0000000..d21578b --- /dev/null +++ b/src/modules/contract/contract.controller.ts @@ -0,0 +1,80 @@ +import { + Body, + Controller, + Get, + Query, + Put, + Delete, + Post, + BadRequestException +} from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Perm, definePermission } from '../auth/decorators/permission.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { ContractService } from './contract.service'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { ContractEntity } from './contract.entity'; +import { ContractDto, ContractQueryDto, ContractUpdateDto } from './contract.dto'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { Domain, SkDomain } from '~/common/decorators/domain.decorator'; +export const permissions = definePermission('app:contract', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('Contract - 合同') +@ApiSecurityAuth() +@Controller('contract') +export class ContractController { + constructor(private contractService: ContractService) {} + + @Get() + @ApiOperation({ summary: '获取合同列表' }) + @ApiResult({ type: [ContractEntity], isPage: true }) + @Perm(permissions.LIST) + async list(@Domain() domain: SkDomain, @Query() dto: ContractQueryDto) { + return this.contractService.findAll({ ...dto, domain }); + } + + @Get(':id') + @ApiOperation({ summary: '获取合同信息' }) + @ApiResult({ type: ContractDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.contractService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增合同' }) + @Perm(permissions.CREATE) + async create(@Domain() domain: SkDomain, @Body() dto: ContractDto): Promise { + await this.contractService.create({ ...dto, domain }); + } + + @Put(':id') + @ApiOperation({ summary: '更新合同' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: ContractUpdateDto): Promise { + await this.contractService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除合同' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.contractService.delete(id); + } + + @Put('unlink-attachments/:id') + @ApiOperation({ summary: '附件解除关联' }) + @Perm(permissions.UPDATE) + async unlinkAttachments( + @IdParam() id: number, + @Body() { fileIds }: ContractUpdateDto + ): Promise { + await this.contractService.unlinkAttachments(id, fileIds); + } +} diff --git a/src/modules/contract/contract.dto.ts b/src/modules/contract/contract.dto.ts new file mode 100644 index 0000000..46e6e85 --- /dev/null +++ b/src/modules/contract/contract.dto.ts @@ -0,0 +1,72 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { + IsArray, + IsDate, + IsDateString, + IsEnum, + IsIn, + IsInt, + IsNumber, + IsOptional, + IsString, + Matches, + MinLength +} from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { Storage } from '../tools/storage/storage.entity'; +import { ContractStatusEnum } from '~/constants/enum'; +import { DomainType, SkDomain } from '~/common/decorators/domain.decorator'; + +export class ContractDto extends DomainType { + @ApiProperty({ description: '合同编号' }) + @Matches(/^[a-z0-9A-Z]+$/, { message: '合同编号只能包含字母和数字' }) + @IsString() + contractNumber: string; + + @ApiProperty({ description: '合同标题' }) + @IsString() + title: string; + + @ApiProperty({ description: '合同类型' }) + @IsNumber() + type: number; + + @ApiProperty({ description: '甲方' }) + @IsString() + partyA: string; + + @ApiProperty({ description: '乙方' }) + @IsString() + partyB: string; + + @ApiProperty({ description: '签订日期' }) + @IsOptional() + @IsDateString() + signingDate?: string; + + @ApiProperty({ description: '交付期限' }) + @IsOptional() + @IsDateString() + deliveryDeadline?: string; + + @ApiProperty({ description: '审核状态(字典)' }) + @IsOptional() + @IsEnum(ContractStatusEnum) + status: number; + + @ApiProperty({ description: '附件' }) + files: Storage[]; +} + +export class ContractUpdateDto extends PartialType(ContractDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} +export class ContractQueryDto extends IntersectionType( + PagerDto, + PartialType(ContractDto), + DomainType +) {} + diff --git a/src/modules/contract/contract.entity.ts b/src/modules/contract/contract.entity.ts new file mode 100644 index 0000000..40856b7 --- /dev/null +++ b/src/modules/contract/contract.entity.ts @@ -0,0 +1,62 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinTable, ManyToMany, Relation } from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; +import { Storage } from '../tools/storage/storage.entity'; +import { SkDomain } from '~/common/decorators/domain.decorator'; + +@Entity({ name: 'contract' }) +export class ContractEntity extends CommonEntity { + @Column({ + name: 'contract_number', + type: 'varchar', + length: 255, + unique: true, + comment: '合同编号' + }) + @ApiProperty({ description: '合同编号' }) + contractNumber: string; + + @Column({ name: 'title', type: 'varchar', length: 255, comment: '合同标题' }) + @ApiProperty({ description: '合同标题' }) + title: string; + + @Column({ type: 'int', comment: '合同类型(字典)' }) + @ApiProperty({ description: '合同类型(字典)' }) + type: number; + + @Column({ name: 'party_a', length: 255, type: 'varchar', comment: '甲方' }) + @ApiProperty({ description: '甲方' }) + partyA: string; + + @Column({ name: 'party_b', length: 255, type: 'varchar', comment: '乙方' }) + @ApiProperty({ description: '乙方' }) + partyB: string; + + @Column({ name: 'signing_date', type: 'date', nullable: true }) + @ApiProperty({ description: '签订日期' }) + signingDate: Date; + + @Column({ name: 'delivery_deadline', type: 'date', nullable: true }) + @ApiProperty({ description: '交付期限' }) + deliveryDeadline: Date; + + @Column({ name: 'status', type: 'tinyint', default: 0, comment: '审核状态(字典)' }) + @ApiProperty({ description: '审核状态:0待审核,1同意,2.不同意(字典)' }) + status: number; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; + + @Column({ type: 'int', default: 1, comment: '所属域' }) + @ApiProperty({ description: '所属域' }) + domain: SkDomain; + + @ManyToMany(() => Storage, storage => storage.contracts) + @JoinTable({ + name: 'contract_storage', + joinColumn: { name: 'contract_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } + }) + files: Relation; +} diff --git a/src/modules/contract/contract.module.ts b/src/modules/contract/contract.module.ts new file mode 100644 index 0000000..d5eabe8 --- /dev/null +++ b/src/modules/contract/contract.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { ContractController } from './contract.controller'; +import { ContractService } from './contract.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ContractEntity } from './contract.entity'; +import { StorageModule } from '../tools/storage/storage.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([ContractEntity]), StorageModule], + controllers: [ContractController], + providers: [ContractService] +}) +export class ContractModule {} diff --git a/src/modules/contract/contract.service.ts b/src/modules/contract/contract.service.ts new file mode 100644 index 0000000..267ef0b --- /dev/null +++ b/src/modules/contract/contract.service.ts @@ -0,0 +1,145 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { ContractEntity } from './contract.entity'; +import { EntityManager, Like, Not, Repository } from 'typeorm'; +import { ContractDto, ContractQueryDto, ContractUpdateDto } from './contract.dto'; +import { Pagination } from '~/helper/paginate/pagination'; +import { isNumber } from 'lodash'; +import { paginate } from '~/helper/paginate'; +import { Storage } from '../tools/storage/storage.entity'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { fieldSearch } from '~/shared/database/field-search'; +import { SkDomain } from '~/common/decorators/domain.decorator'; + +@Injectable() +export class ContractService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(ContractEntity) + private contractRepository: Repository, + @InjectRepository(Storage) + private storageRepository: Repository + ) {} + + /** + * 查找所有合同 + */ + async findAll({ + page, + pageSize, + ...fields + }: ContractQueryDto): Promise> { + const queryBuilder = this.contractRepository + .createQueryBuilder('contract') + .leftJoin('contract.files', 'files') + .addSelect(['files.id', 'files.path']) + .where(fieldSearch(fields)) + .andWhere('contract.isDelete = 0'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create({ contractNumber, ...ext }: ContractDto): Promise { + if (await this.checkIsContractNumberExsit(contractNumber)) { + throw new BusinessException(ErrorEnum.CONTRACT_NUMBER_EXIST); + } + await this.contractRepository.insert( + this.contractRepository.create({ contractNumber, ...ext }) + ); + } + + /** + * 更新 + */ + async update( + id: number, + { fileIds, contractNumber, ...ext }: Partial + ): Promise { + await this.entityManager.transaction(async manager => { + if (contractNumber && (await this.checkIsContractNumberExsit(contractNumber, id))) { + throw new BusinessException(ErrorEnum.CONTRACT_NUMBER_EXIST); + } + await manager.update(ContractEntity, id, { + ...ext, + contractNumber + }); + + if (fileIds?.length) { + const count = await this.storageRepository + .createQueryBuilder('storage') + .where('storage.id in(:fileIds)', { fileIds }) + .getCount(); + if (count !== fileIds?.length) { + throw new BusinessException(ErrorEnum.STORAGE_NOT_FOUND); + } + // 附件要批量插入 + await manager.createQueryBuilder().relation(ContractEntity, 'files').of(id).add(fileIds); + } + }); + } + + /** + * 是否存在相同编号的合同 + * @param contractNumber 合同编号 + */ + async checkIsContractNumberExsit(contractNumber: string, id?: number): Promise { + return !!(await this.contractRepository.findOne({ + where: { + contractNumber: contractNumber, + id: Not(id) + } + })); + } + /** + * 删除 + */ + async delete(id: number): Promise { + // 合同比较重要,做逻辑删除 + await this.contractRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个合同信息 + */ + async info(id: number) { + const info = await this.contractRepository + .createQueryBuilder('contract') + .where({ + id + }) + .andWhere('contract.isDelete = 0') + .getOne(); + return info; + } + + /** + * 解除附件关联 + * @param id 合同ID + * @param fileIds 附件ID + */ + async unlinkAttachments(id: number, fileIds: number[]) { + await this.entityManager.transaction(async manager => { + const contract = await this.contractRepository + .createQueryBuilder('contract') + .leftJoinAndSelect('contract.files', 'files') + .where('contract.id = :id', { id }) + .getOne(); + const linkedFiles = contract.files + .map(item => item.id) + .filter(item => !fileIds.includes(item)); + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(ContractEntity, 'files') + .of(id) + .addAndRemove(linkedFiles, contract.files); + }); + } +} diff --git a/src/modules/domian/domain.controller.ts b/src/modules/domian/domain.controller.ts new file mode 100644 index 0000000..0bd02a3 --- /dev/null +++ b/src/modules/domian/domain.controller.ts @@ -0,0 +1,67 @@ +import { + Body, + Controller, + Get, + Query, + Put, + Delete, + Post, + BadRequestException +} from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Perm, definePermission } from '../auth/decorators/permission.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { DomainService } from './domain.service'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { DomainEntity } from './domain.entity'; +import { DomainDto, DomainQueryDto, DomainUpdateDto } from './domain.dto'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +export const permissions = definePermission('app:domain', { + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('Domain - 域') +@ApiSecurityAuth() +@Controller('domain') +export class DomainController { + constructor(private domainService: DomainService) {} + + @Get() + @ApiOperation({ summary: '获取域列表' }) + @ApiResult({ type: [DomainEntity], isPage: true }) + async list(@Query() dto: DomainQueryDto) { + return this.domainService.findAll(dto); + } + + @Get(':id') + @ApiOperation({ summary: '获取域信息' }) + @ApiResult({ type: DomainDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.domainService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增域' }) + @Perm(permissions.CREATE) + async create(@Body() dto: DomainDto): Promise { + await this.domainService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新域' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: DomainUpdateDto): Promise { + await this.domainService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除域' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.domainService.delete(id); + } +} diff --git a/src/modules/domian/domain.dto.ts b/src/modules/domian/domain.dto.ts new file mode 100644 index 0000000..61d94fb --- /dev/null +++ b/src/modules/domian/domain.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; + +export class DomainDto { + @ApiProperty({ description: '域标题' }) + @IsString() + title: string; +} + +export class DomainUpdateDto extends PartialType(DomainDto) {} +export class DomainQueryDto extends IntersectionType(PagerDto, PartialType(DomainDto)) {} diff --git a/src/modules/domian/domain.entity.ts b/src/modules/domian/domain.entity.ts new file mode 100644 index 0000000..8ed3c48 --- /dev/null +++ b/src/modules/domian/domain.entity.ts @@ -0,0 +1,15 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinTable, ManyToMany, Relation } from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; +import { Storage } from '../tools/storage/storage.entity'; + +@Entity({ name: 'domain' }) +export class DomainEntity extends CommonEntity { + @Column({ name: 'title', type: 'varchar', length: 255, comment: '域标题' }) + @ApiProperty({ description: '域标题' }) + title: string; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; +} diff --git a/src/modules/domian/domain.module.ts b/src/modules/domian/domain.module.ts new file mode 100644 index 0000000..9aaa16f --- /dev/null +++ b/src/modules/domian/domain.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { DomainController } from './domain.controller'; +import { DomainService } from './domain.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DomainEntity } from './domain.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([DomainEntity])], + controllers: [DomainController], + providers: [DomainService] +}) +export class DomainModule {} diff --git a/src/modules/domian/domain.service.ts b/src/modules/domian/domain.service.ts new file mode 100644 index 0000000..7fdfecd --- /dev/null +++ b/src/modules/domian/domain.service.ts @@ -0,0 +1,95 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { DomainEntity } from './domain.entity'; +import { EntityManager, Like, Not, Repository } from 'typeorm'; +import { DomainDto, DomainQueryDto, DomainUpdateDto } from './domain.dto'; +import { Pagination } from '~/helper/paginate/pagination'; +import { isNumber } from 'lodash'; +import { paginate } from '~/helper/paginate'; +import { Storage } from '../tools/storage/storage.entity'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { fieldSearch } from '~/shared/database/field-search'; + +@Injectable() +export class DomainService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(DomainEntity) + private domainRepository: Repository + ) {} + + /** + * 查找所有域 + */ + async findAll({ page, pageSize, ...fields }: DomainQueryDto): Promise> { + const queryBuilder = this.domainRepository + .createQueryBuilder('domain') + .where(fieldSearch(fields)) + .andWhere('domain.isDelete = 0'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create({ title, ...ext }: DomainDto): Promise { + if (await this.checkIsDomainExsit(title)) { + throw new BusinessException(ErrorEnum.DOMAIN_TITLE_DUPLICATE); + } + await this.domainRepository.insert(this.domainRepository.create({ title, ...ext })); + } + + /** + * 更新 + */ + async update(id: number, { title, ...ext }: Partial): Promise { + await this.entityManager.transaction(async manager => { + if (title && (await this.checkIsDomainExsit(title, id))) { + throw new BusinessException(ErrorEnum.CONTRACT_NUMBER_EXIST); + } + await manager.update(DomainEntity, id, { + ...ext, + title + }); + }); + } + + /** + * 是否存在相同的域 + * @param title 域编号 + */ + async checkIsDomainExsit(title: string, id?: number): Promise { + return !!(await this.domainRepository.findOne({ + where: { + title: title, + id: Not(id) + } + })); + } + /** + * 删除 + */ + async delete(id: number): Promise { + // 域比较重要,做逻辑删除 + await this.domainRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个域信息 + */ + async info(id: number) { + const info = await this.domainRepository + .createQueryBuilder('domain') + .where({ + id + }) + .andWhere('domain.isDelete = 0') + .getOne(); + return info; + } +} diff --git a/src/modules/health/health.controller.ts b/src/modules/health/health.controller.ts new file mode 100644 index 0000000..6927b4c --- /dev/null +++ b/src/modules/health/health.controller.ts @@ -0,0 +1,71 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { + DiskHealthIndicator, + HealthCheck, + HttpHealthIndicator, + MemoryHealthIndicator, + TypeOrmHealthIndicator +} from '@nestjs/terminus'; + +import { Perm, definePermission } from '../auth/decorators/permission.decorator'; + +export const PermissionHealth = definePermission('app:health', { + NETWORK: 'network', + DB: 'database', + MH: 'memory-heap', + MR: 'memory-rss', + DISK: 'disk' +} as const); + +@ApiTags('Health - 健康检查') +@Controller('health') +export class HealthController { + constructor( + private http: HttpHealthIndicator, + private db: TypeOrmHealthIndicator, + private memory: MemoryHealthIndicator, + private disk: DiskHealthIndicator + ) {} + + @Get('network') + @HealthCheck() + @Perm(PermissionHealth.NETWORK) + async checkNetwork() { + return this.http.pingCheck('louis', 'https://gitee.com/lu-zixun'); + } + + @Get('database') + @HealthCheck() + @Perm(PermissionHealth.DB) + async checkDatabase() { + return this.db.pingCheck('database'); + } + + @Get('memory-heap') + @HealthCheck() + @Perm(PermissionHealth.MH) + async checkMemoryHeap() { + // the process should not use more than 200MB memory + return this.memory.checkHeap('memory-heap', 200 * 1024 * 1024); + } + + @Get('memory-rss') + @HealthCheck() + @Perm(PermissionHealth.MR) + async checkMemoryRSS() { + // the process should not have more than 200MB RSS memory allocated + return this.memory.checkRSS('memory-rss', 200 * 1024 * 1024); + } + + @Get('disk') + @HealthCheck() + @Perm(PermissionHealth.DISK) + async checkDisk() { + return this.disk.checkStorage('disk', { + // The used disk storage should not exceed 75% of the full disk size + thresholdPercent: 0.75, + path: '/' + }); + } +} diff --git a/src/modules/health/health.module.ts b/src/modules/health/health.module.ts new file mode 100644 index 0000000..674c072 --- /dev/null +++ b/src/modules/health/health.module.ts @@ -0,0 +1,11 @@ +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; +import { TerminusModule } from '@nestjs/terminus'; + +import { HealthController } from './health.controller'; + +@Module({ + imports: [TerminusModule, HttpModule], + controllers: [HealthController] +}) +export class HealthModule {} diff --git a/src/modules/materials_inventory/in_out/materials_in_out.controller.ts b/src/modules/materials_inventory/in_out/materials_in_out.controller.ts new file mode 100644 index 0000000..c41a04d --- /dev/null +++ b/src/modules/materials_inventory/in_out/materials_in_out.controller.ts @@ -0,0 +1,90 @@ +import { Body, Controller, Delete, Get, Post, Put, Query, Res } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { MaterialsInOutService } from './materials_in_out.service'; +import { MaterialsInOutEntity } from './materials_in_out.entity'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { definePermission, Perm } from '~/modules/auth/decorators/permission.decorator'; +import { + MaterialsInOutQueryDto, + MaterialsInOutDto, + MaterialsInOutUpdateDto, + MaterialsInOutExportDto +} from './materials_in_out.dto'; +import { Domain, DomainType, SkDomain } from '~/common/decorators/domain.decorator'; +import { FastifyReply } from 'fastify'; +export const permissions = definePermission('materials_inventory:history_in_out', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete', + EXPORT: 'export' +} as const); + +@ApiTags('Materials In Out History - 原材料出入库记录') +@ApiSecurityAuth() +@Controller('materials-in-out') +export class MaterialsInOutController { + constructor(private materialsInOutService: MaterialsInOutService) { } + + @Get('export') + @ApiOperation({ summary: '导出原材料盘点表' }) + @Perm(permissions.EXPORT) + async exportMaterialsInventoryCheck( + @Domain() domain: SkDomain, + @Query() dto: MaterialsInOutExportDto, + @Res() res: FastifyReply + ): Promise { + await this.materialsInOutService.exportMaterialsInventoryCheck({ ...dto, domain }, res); + } + + + @Get() + @ApiOperation({ summary: '获取原材料出入库记录列表' }) + @ApiResult({ type: [MaterialsInOutEntity], isPage: true }) + @Perm(permissions.LIST) + async list(@Domain() domain: SkDomain, @Query() dto: MaterialsInOutQueryDto) { + return this.materialsInOutService.findAll({ ...dto, domain }); + } + + @Get(':id') + @ApiOperation({ summary: '获取原材料出入库记录信息' }) + @ApiResult({ type: MaterialsInOutDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.materialsInOutService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增原材料出入库记录' }) + @Perm(permissions.CREATE) + async create(@Domain() domain: SkDomain, @Body() dto: MaterialsInOutDto): Promise { + return this.materialsInOutService.create({ ...dto, domain }); + } + + @Put(':id') + @ApiOperation({ summary: '更新原材料出入库记录' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: MaterialsInOutUpdateDto): Promise { + await this.materialsInOutService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除原材料出入库记录' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.materialsInOutService.delete(id); + } + + @Put('unlink-attachments/:id') + @ApiOperation({ summary: '附件解除关联' }) + @Perm(permissions.UPDATE) + async unlinkAttachments( + @IdParam() id: number, + @Body() { fileIds }: MaterialsInOutUpdateDto + ): Promise { + await this.materialsInOutService.unlinkAttachments(id, fileIds); + } +} diff --git a/src/modules/materials_inventory/in_out/materials_in_out.dto.ts b/src/modules/materials_inventory/in_out/materials_in_out.dto.ts new file mode 100644 index 0000000..f4faab5 --- /dev/null +++ b/src/modules/materials_inventory/in_out/materials_in_out.dto.ts @@ -0,0 +1,199 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { + IsArray, + IsBoolean, + IsDate, + IsDateString, + IsEnum, + IsIn, + IsInt, + IsNumber, + IsOptional, + IsString, + Matches, + MinLength, + ValidateIf, + isNumber +} from 'class-validator'; +import dayjs from 'dayjs'; +import { DomainType } from '~/common/decorators/domain.decorator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { MaterialsInOrOutEnum } from '~/constants/enum'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { formatToDate } from '~/utils'; + +export class MaterialsInOutDto extends DomainType { + @IsOptional() + @IsNumber() + @ApiProperty({ description: '项目Id' }) + projectId?: number; + + @ApiProperty({ description: '产品Id' }) + @ValidateIf(o => !o.inventoryInOutNumber) + @IsNumber() + productId: number; + + @ApiProperty({ description: '原材料库存编号' }) + @IsOptional() + @IsString() + inventoryInOutNumber: string; + + @ApiProperty({ description: '库存id(产品和单价双主键决定一条库存)' }) + @IsOptional() + @IsNumber() + inventoryId: number; + + @ApiProperty({ description: '单位(字典)' }) + @IsNumber() + @IsOptional() + unitId: number; + + @ApiProperty({ description: '入库或出库 0:入库 1:出库' }) + @IsEnum(MaterialsInOrOutEnum) + inOrOut: MaterialsInOrOutEnum; + + @ApiProperty({ description: '时间' }) + @Transform(params => { + return params.value ? new Date(params.value) : null; + }) + @IsOptional() + time: Date; + + @ApiProperty({ description: '数量' }) + @IsNumber() + quantity: number; + + @ApiProperty({ description: '单价' }) + @IsOptional() + @IsNumber() + unitPrice: number; + + @ApiProperty({ description: '金额' }) + @IsOptional() + @IsNumber() + amount: number; + + @ApiProperty({ description: '经办人' }) + @IsOptional() + @IsString() + agent: string; + + @ApiProperty({ description: '领料单号' }) + @IsOptional() + @IsString() + issuanceNumber: string; + + @ApiProperty({ description: '库存位置' }) + @IsOptional() + @IsString() + position: string; + + @IsOptional() + @IsString() + @ApiProperty({ description: '备注' }) + remark: string; +} + +export class MaterialsInOutUpdateDto extends PartialType(MaterialsInOutDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} +export class MaterialsInOutQueryDto extends IntersectionType( + PagerDto, + DomainType +) { + @ApiProperty({ description: '出入库时间YYYY-MM-DD' }) + @IsOptional() + // @IsString() + @Transform(params => { + // 开始和结束时间用的是一天的开始和一天的结束的时分秒 + return params.value + ? [ + params.value[0] ? `${formatToDate(params.value[0], 'YYYY-MM-DD')} 00:00:00` : null, + params.value[1] ? `${formatToDate(params.value[1], 'YYYY-MM-DD')} 23:59:59` : null + ] + : []; + }) + time?: string[]; + + @ApiProperty({ description: '入库或出库 0:入库 1:出库' }) + @IsOptional() + @IsEnum(MaterialsInOrOutEnum) + inOrOut?: MaterialsInOrOutEnum; + + @ApiProperty({ description: '产品名称' }) + @IsOptional() + @IsString() + product?: string; + + @ApiProperty({ description: '经办人' }) + @IsOptional() + @IsString() + agent?: string; + + @ApiProperty({ description: '领料单号' }) + @IsOptional() + @IsString() + issuanceNumber?: string; + + @ApiProperty({ description: '原材料库存编号' }) + @IsOptional() + @IsString() + inventoryInOutNumber?: string; + + @IsOptional() + @IsString() + @ApiProperty({ description: '备注' }) + remark?: string; + + @IsOptional() + @IsNumber() + @ApiProperty({ description: '项目Id' }) + projectId?: number; + + @IsOptional() + @IsBoolean() + @ApiProperty({ description: '是否是用于创建出库记录' }) + isCreateOut?: boolean; +} +export class MaterialsInOutExportDto extends IntersectionType( + + DomainType +) { + + @ApiProperty({ description: '导出时间YYYY-MM-DD' }) + @IsOptional() + @IsArray() + @Transform(params => { + // 开始和结束时间用的是一月的开始和一月的结束的时分秒 + const date = params.value; + return [ + date ? `${date[0]} 00:00:00` : null, + date ? `${date[1]} 23:59:59` : null + ]; + }) + time?: string[]; + + @ApiProperty({ description: '导出文件名' }) + @IsOptional() + @IsString() + filename?: string + + @ApiProperty({ description: '入库或出库 0:入库 1:出库' }) + @IsOptional() + @IsEnum(MaterialsInOrOutEnum) + inOrOut?: MaterialsInOrOutEnum; + + @ApiProperty({ description: '产品名称' }) + @IsOptional() + @IsString() + product?: string; + + @ApiProperty({ description: '经办人' }) + @IsOptional() + @IsString() + agent?: string; +} \ No newline at end of file diff --git a/src/modules/materials_inventory/in_out/materials_in_out.entity.ts b/src/modules/materials_inventory/in_out/materials_in_out.entity.ts new file mode 100644 index 0000000..b8e36b0 --- /dev/null +++ b/src/modules/materials_inventory/in_out/materials_in_out.entity.ts @@ -0,0 +1,148 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Expose } from 'class-transformer'; +import pinyin from 'pinyin'; +import { + BeforeInsert, + Column, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + Relation, + Repository +} from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; +import { MaterialsInOrOutEnum, ParamConfigEnum } from '~/constants/enum'; +import { ProductEntity } from '~/modules/product/product.entity'; +import { ProjectEntity } from '~/modules/project/project.entity'; +import { ParamConfigEntity } from '~/modules/system/param-config/param-config.entity'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { MaterialsInventoryEntity } from '../materials_inventory.entity'; +import { SkDomain } from '~/common/decorators/domain.decorator'; +@Entity({ name: 'materials_in_out' }) +export class MaterialsInOutEntity extends CommonEntity { + @Column({ + name: 'inventory_inout_number', + type: 'varchar', + length: 50, + comment: '原材料出入库编号' + }) + @ApiProperty({ description: '原材料出入库编号' }) + inventoryInOutNumber: string; + + @Column({ + name: 'product_id', + type: 'int', + comment: '产品' + }) + @ApiProperty({ description: '产品' }) + productId: number; + + @Column({ + name: 'inventory_id', + type: 'int', + comment: '库存' + }) + @ApiProperty({ description: '库存' }) + inventoryId: number; + + @Column({ + name: 'in_or_out', + type: 'tinyint', + comment: '入库或出库' + }) + @ApiProperty({ description: '入库或出库 0:入库 1:出库' }) + inOrOut: MaterialsInOrOutEnum; + + @Column({ + name: 'time', + type: 'datetime', + nullable: true, + comment: '时间' + }) + @ApiProperty({ description: '时间' }) + time: Date; + + @Column({ + name: 'quantity', + type: 'int', + default: 0, + comment: '数量' + }) + @ApiProperty({ description: '数量' }) + quantity: number; + + @Column({ + name: 'unit_price', + type: 'decimal', + precision: 15, + default: 0, + scale: 10, + comment: '单价' + }) + @ApiProperty({ description: '单价' }) + unitPrice: number; + + @Column({ + name: 'amount', + type: 'decimal', + precision: 15, + default: 0, + scale: 10, + comment: '金额' + }) + @ApiProperty({ description: '金额' }) + amount: number; + + @Column({ name: 'agent', type: 'varchar', length: 50, comment: '经办人', nullable: true }) + @ApiProperty({ description: '经办人' }) + agent: string; + + @Column({ + name: 'issuance_number', + type: 'varchar', + length: 100, + nullable: true, + comment: '领料单号' + }) + @ApiProperty({ description: '领料单号' }) + issuanceNumber: string; + + @Column({ name: 'remark', type: 'varchar', length: 255, comment: '备注', nullable: true }) + @ApiProperty({ description: '备注' }) + remark: string; + + @Column({ name: 'project_id', type: 'int', comment: '项目', nullable: true }) + @ApiProperty({ description: '项目Id' }) + projectId: number; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; + + @ManyToOne(() => ProjectEntity) + @JoinColumn({ name: 'project_id' }) + project: ProjectEntity; + + @ManyToOne(() => ProductEntity) + @JoinColumn({ name: 'product_id' }) + product: ProductEntity; + + @Column({ type: 'int', default: 1, comment: '所属域' }) + @ApiProperty({ description: '所属域' }) + domain: SkDomain; + + @ManyToMany(() => Storage, storage => storage.materialsInOuts) + @JoinTable({ + name: 'materials_in_out_storage', + joinColumn: { name: 'materials_in_out_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } + }) + files: Relation; + + @ManyToOne(() => MaterialsInventoryEntity) + @JoinColumn({ name: 'inventory_id' }) + inventory: MaterialsInventoryEntity; +} diff --git a/src/modules/materials_inventory/in_out/materials_in_out.service.ts b/src/modules/materials_inventory/in_out/materials_in_out.service.ts new file mode 100644 index 0000000..5cc4ef6 --- /dev/null +++ b/src/modules/materials_inventory/in_out/materials_in_out.service.ts @@ -0,0 +1,465 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; + +import { Between, EntityManager, In, Repository } from 'typeorm'; +import { Pagination } from '~/helper/paginate/pagination'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { paginate } from '~/helper/paginate'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { + MaterialsInOutQueryDto, + MaterialsInOutDto, + MaterialsInOutUpdateDto, + MaterialsInOutExportDto +} from './materials_in_out.dto'; +import { MaterialsInOutEntity } from './materials_in_out.entity'; +import { fieldSearch } from '~/shared/database/field-search'; +import { ParamConfigEntity } from '~/modules/system/param-config/param-config.entity'; +import { MaterialsInOrOutEnum, ParamConfigEnum } from '~/constants/enum'; +import { MaterialsInventoryEntity } from '../materials_inventory.entity'; +import { MaterialsInventoryService } from '../materials_inventory.service'; +import { isDefined } from 'class-validator'; +import { FastifyReply } from 'fastify'; +import * as ExcelJS from 'exceljs'; +import dayjs from 'dayjs'; +@Injectable() +export class MaterialsInOutService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(MaterialsInOutEntity) + private materialsInOutRepository: Repository, + @InjectRepository(Storage) + private storageRepository: Repository, + @InjectRepository(ParamConfigEntity) + private paramConfigRepository: Repository, + private materialsInventoryService: MaterialsInventoryService + ) { } + + + /** + * 导出出入库记录表 + */ + async exportMaterialsInventoryCheck( + { time, domain, filename, ...ext }: MaterialsInOutExportDto, + res: FastifyReply + ): Promise { + const ROW_HEIGHT = 20; + const HEADER_FONT_SIZE = 18; + + // 生成数据 + const sqb = this.buildSearchQuery() + .where(fieldSearch(ext)) + .andWhere({ + time: Between(time[0], time[1]) + }) + .andWhere('materialsInOut.isDelete = 0'); + const data = await sqb.addOrderBy('materialsInOut.time', 'DESC').getMany(); + const workbook = new ExcelJS.Workbook(); + const sheet = workbook.addWorksheet('出入库记录'); + sheet.mergeCells('A1:T1'); + // 设置标题 + sheet.getCell('A1').value = '山东矿机华信智能科技有限公司出入库记录表'; + // 设置日期 + sheet.mergeCells('A2:C2'); + sheet.getCell('A2').value = `日期:${dayjs(time[0]).format('YYYY年M月D日')}-${dayjs(time[1]).format('YYYY年M月D日')}`; + // 设置表头 + const headers = [ + '出入库单号', + '出入库', + '项目', + '公司名称', + '产品名称', + '规格型号', + '时间', + '单位', + '数量', + '单价', + '金额', + '经办人', + '领料单号', + '备注' + ]; + sheet.addRow(headers); + for (let index = 0; index < data.length; index++) { + const record = data[index]; + sheet.addRow([ + `${record.inventoryInOutNumber}`, + record.project?.name || '', + record.inOrOut === MaterialsInOrOutEnum.In ? '入库' : "出库", + record.product?.company?.name || '', + record.product?.name || '', + record.product?.productSpecification || '', + `${dayjs(record.time).format('YYYY-MM-DD HH:mm')}`, + record.product.unit.label || '', + record.quantity, + parseFloat(`${record.unitPrice || 0}`), + parseFloat(`${record.amount || 0}`), + `${record?.agent || ''}`, + record?.issuanceNumber || '', + record?.remark || '' + ]); + } + // 固定信息样式设定 + sheet.eachRow((row, index) => { + if (index >= 3) { + row.alignment = { vertical: 'middle', horizontal: 'center' }; + row.height = ROW_HEIGHT; + row.eachCell(cell => { + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + }; + }); + } + }); + + sheet.columns.forEach((column, index: number) => { + let maxColumnLength = 0; + const autoWidth = ['B', 'C', 'S', 'U']; + if (String.fromCharCode(65 + index) === 'B') maxColumnLength = 20; + if (autoWidth.includes(String.fromCharCode(65 + index))) { + column.eachCell({ includeEmpty: true }, (cell, rowIndex) => { + if (rowIndex >= 5) { + const columnLength = `${cell.value || ''}`.length; + if (columnLength > maxColumnLength) { + maxColumnLength = columnLength; + } + } + }); + column.width = maxColumnLength < 12 ? 12 : maxColumnLength; // Minimum width of 10 + } else { + column.width = 12; + } + }); + //读取buffer进行传输 + const buffer = await workbook.xlsx.writeBuffer(); + res + .header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + .header( + 'Content-Disposition', + `attachment; filename="${filename}.xls"` + ) + .send(buffer); + } + + + + /** + * 查询所有出入库记录 + */ + async findAll({ + page, + pageSize, + product: productName, + projectId, + isCreateOut, + ...ext + }: MaterialsInOutQueryDto): Promise> { + const sqb = this.buildSearchQuery() + .where(fieldSearch(ext)) + .andWhere('materialsInOut.isDelete = 0') + .addOrderBy('materialsInOut.createdAt', 'DESC'); + + if (productName) { + sqb.andWhere('product.name like :productName', { productName: `%${productName}%` }); + } + + if (projectId) { + sqb.andWhere('project.id = :projectId', { projectId }); + } + + if (isCreateOut) { + sqb.andWhere('materialsInOut.inOrOut = 0'); + } + const pageData = await paginate(sqb, { + page, + pageSize + }); + return pageData; + } + + buildSearchQuery() { + return this.materialsInOutRepository + .createQueryBuilder('materialsInOut') + .leftJoin('materialsInOut.files', 'files') + .leftJoin('materialsInOut.project', 'project') + .leftJoin('materialsInOut.product', 'product') + .leftJoin('materialsInOut.inventory', 'inventory') + .leftJoin('product.unit', 'unit') + .leftJoin('product.files', 'productFiles') + .leftJoin('product.company', 'company') + .addSelect([ + 'inventory.id', + 'inventory.position', + 'inventory.inventoryNumber', + 'files.id', + 'files.path', + 'project.name', + 'product.name', + 'product.productSpecification', + 'product.productNumber', + 'productFiles.id', + 'productFiles.path', + 'unit.label', + 'company.name' + ]); + } + /** + * 新增 + */ + async create(dto: MaterialsInOutDto): Promise { + let { + inOrOut, + inventoryInOutNumber, + projectId, + inventoryId, + position, + unitPrice, + quantity, + productId, + domain + } = dto; + inventoryInOutNumber = await this.generateInventoryInOutNumber(inOrOut); + let newRecordId; + await this.entityManager.transaction(async manager => { + delete dto.position; + // 1.更新增减库存 + const inventoryEntity = await ( + Object.is(inOrOut, MaterialsInOrOutEnum.In) + ? this.materialsInventoryService.inInventory.bind(this.materialsInventoryService) + : this.materialsInventoryService.outInventory.bind(this.materialsInventoryService) + )({ productId, quantity, unitPrice, projectId, inventoryId, position }, manager, domain); + // 2.生成出入库记录 + const { id } = await manager.save(MaterialsInOutEntity, { + ...this.materialsInOutRepository.create({ ...dto, inventoryId: inventoryEntity?.id }), + inventoryInOutNumber + }); + newRecordId = id; + }); + return newRecordId; + } + + /** + * 更新 + */ + async update(id: number, { fileIds, ...data }: Partial): Promise { + await this.entityManager.transaction(async manager => { + /* 暂时不允许更改金额和数量,以及不能影响库存变化, */ + const entity = await manager.findOne(MaterialsInOutEntity, { + where: { + id + }, + lock: { mode: 'pessimistic_write' } + }); + + // 修改入库记录的价格 + // 1.会直接更改库存实际价格.(仅仅只能之前价格为0时可以修改) + // 2.会同步库存所有的出库记录,修改其单价和金额. + if ( + Object.is(data.inOrOut, MaterialsInOrOutEnum.In) && + isDefined(data.unitPrice) && + Math.abs(Number(data.unitPrice) - Number(entity.unitPrice)) !== 0 + ) { + if (entity.unitPrice != 0) { + throw new BusinessException( + ErrorEnum.MATERIALS_IN_OUT_UNIT_PRICE_MUST_ZERO_WHEN_MODIFIED + ); + } + const outEntities = await manager.find(MaterialsInOutEntity, { + where: { + inventoryId: entity.inventoryId, + inOrOut: MaterialsInOrOutEnum.Out + } + }); + if (outEntities?.length > 0) { + await manager.update( + MaterialsInOutEntity, + { + id: In(outEntities.map(item => item.id)) + }, + { + unitPrice: data.unitPrice, + amount: () => `quantity * ${data.unitPrice}` + } + ); + } + await manager.update(MaterialsInventoryEntity, entity.inventoryId, { + unitPrice: data.unitPrice + }); + } + // 修改入库时的项目,必须同步到库存项目中 + if ( + Object.is(data.inOrOut, MaterialsInOrOutEnum.In) && + isDefined(data.projectId) && + data.projectId != entity.projectId + ) { + await manager.update(MaterialsInventoryEntity, entity.inventoryId, { + projectId: data.projectId + }); + } + + // 暂时不允许修改数量 + // let changedQuantity = 0; + // if (isDefined(data.quantity) && entity.quantity !== data.quantity) { + // if (entity.inOrOut === MaterialsInOrOutEnum.In) { + // // 入库减少等于出库 + // if (data.quantity - entity.quantity < 0) { + // data.inOrOut = MaterialsInOrOutEnum.Out; + // } else { + // // 入库增多等于入库 + // data.inOrOut = MaterialsInOrOutEnum.In; + // } + // } else { + // // 出库减少等于入库 + // if (data.quantity - entity.quantity < 0) { + // data.inOrOut = MaterialsInOrOutEnum.In; + // } else { + // // 出库增多等于出库 + // data.inOrOut = MaterialsInOrOutEnum.Out; + // } + // } + // changedQuantity = Math.abs(data.quantity - entity.quantity); + // } + // // 2.更新增减库存 + // if (changedQuantity !== 0) { + // await ( + // Object.is(data.inOrOut, MaterialsInOrOutEnum.In) + // ? this.materialsInventoryService.inInventory + // : this.materialsInventoryService.outInventory + // )( + // { + // productId: entity.productId, + // quantity: Math.abs(changedQuantity), + // unitPrice: undefined, + // projectId: entity.projectId + // }, + // manager + // ); + // } + // 完成所有业务逻辑后,更新出入库记录 + await manager.update(MaterialsInOutEntity, id, { + ...data + }); + + if (fileIds?.length) { + const count = await this.storageRepository + .createQueryBuilder('storage') + .where('storage.id in(:fileIds)', { fileIds }) + .getCount(); + if (count !== fileIds?.length) { + throw new BusinessException(ErrorEnum.STORAGE_NOT_FOUND); + } + // 附件要批量插入 + await manager + .createQueryBuilder() + .relation(MaterialsInOutEntity, 'files') + .of(id) + .add(fileIds); + } + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + await this.entityManager.transaction(async manager => { + const entity = await manager.findOne(MaterialsInOutEntity, { + where: { + id, + isDelete: 0 + }, + lock: { mode: 'pessimistic_write' } + }); + if (!entity) { + throw new BusinessException(ErrorEnum.MATERIALS_IN_OUT_NOT_FOUND); + } + + // 更新库存 + await ( + Object.is(entity.inOrOut, MaterialsInOrOutEnum.In) + ? this.materialsInventoryService.outInventory.bind(this.materialsInventoryService) + : this.materialsInventoryService.inInventory.bind(this.materialsInventoryService) + )( + { + quantity: entity.quantity, + inventoryId: entity.inventoryId + }, + manager + ); + }); + + // 出入库比较重要,做逻辑删除 + await this.materialsInOutRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个出入库信息 + */ + async info(id: number) { + const info = await this.buildSearchQuery() + .where({ + id + }) + .andWhere('materialsInOut.isDelete = 0') + .getOne(); + return info; + } + + /** + * 解除附件关联 + * @param id 出入库ID + * @param fileIds 附件ID + */ + async unlinkAttachments(id: number, fileIds: number[]) { + await this.entityManager.transaction(async manager => { + const materialsInOut = await this.materialsInOutRepository + .createQueryBuilder('materialsInOut') + .leftJoinAndSelect('materialsInOut.files', 'files') + .where('materialsInOut.id = :id', { id }) + .getOne(); + const linkedFiles = materialsInOut.files + .map(item => item.id) + .filter(item => !fileIds.includes(item)); + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(MaterialsInOutEntity, 'files') + .of(id) + .addAndRemove(linkedFiles, materialsInOut.files); + }); + } + + /** + * 生成库存出入库单号 + * @returns 库存出入库单号 + */ + async generateInventoryInOutNumber(inOrOut: MaterialsInOrOutEnum = MaterialsInOrOutEnum.In) { + const prefix = + ( + await this.paramConfigRepository.findOne({ + where: { + key: inOrOut + ? ParamConfigEnum.InventoryInOutNumberPrefixOut + : ParamConfigEnum.InventoryInOutNumberPrefixIn + } + }) + )?.value || ''; + const lastMaterial = await this.materialsInOutRepository + .createQueryBuilder('materialsInOut') + .select( + `MAX(CAST(REPLACE(materialsInOut.inventoryInOutNumber, '${prefix}', '') AS UNSIGNED))`, + 'maxInventoryInOutNumber' + ) + .where('materialsInOut.inOrOut = :inOrOut', { inOrOut }) + .getRawOne(); + const lastNumber = lastMaterial.maxInventoryInOutNumber + ? parseInt(lastMaterial.maxInventoryInOutNumber.replace(prefix, '')) + : 0; + const newNumber = lastNumber + 1 < 1000 ? 1000 : lastNumber + 1; + return `${prefix}${newNumber}`; + } +} diff --git a/src/modules/materials_inventory/materials_inventory.controller.ts b/src/modules/materials_inventory/materials_inventory.controller.ts new file mode 100644 index 0000000..8e53f13 --- /dev/null +++ b/src/modules/materials_inventory/materials_inventory.controller.ts @@ -0,0 +1,80 @@ +import { Body, Controller, Delete, Get, Param, Post, Put, Query, Res } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { Perm, definePermission } from '../auth/decorators/permission.decorator'; +import { + MaterialsInventoryQueryDto, + MaterialsInventoryDto, + MaterialsInventoryUpdateDto, + MaterialsInventoryExportDto +} from '../materials_inventory/materials_inventory.dto'; +import { MaterialsInventoryService } from './materials_inventory.service'; +import { MaterialsInventoryEntity } from './materials_inventory.entity'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { FastifyReply } from 'fastify'; +import { Domain, DomainType, SkDomain } from '~/common/decorators/domain.decorator'; + +export const permissions = definePermission('app:materials_inventory', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete', + EXPORT: 'export' +} as const); + +@ApiTags('MaterialsI Inventory - 原材料库存') +@ApiSecurityAuth() +@Controller('materials-inventory') +export class MaterialsInventoryController { + constructor(private miService: MaterialsInventoryService) {} + + @Get('export') + @ApiOperation({ summary: '导出原材料盘点表' }) + @Perm(permissions.EXPORT) + async exportMaterialsInventoryCheck( + @Domain() domain: SkDomain, + @Query() dto: MaterialsInventoryExportDto, + @Res() res: FastifyReply + ): Promise { + await this.miService.exportMaterialsInventoryCheck({ ...dto, domain }, res); + } + + @Get() + @ApiOperation({ summary: '获取原材料库存列表' }) + @ApiResult({ type: [MaterialsInventoryEntity], isPage: true }) + @Perm(permissions.LIST) + async list(@Domain() domain: SkDomain, @Query() dto: MaterialsInventoryQueryDto) { + return this.miService.findAll({ ...dto, domain }); + } + + @Get(':id') + @ApiOperation({ summary: '获取原材料库存信息' }) + @ApiResult({ type: MaterialsInventoryDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.miService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增原材料库存' }) + @Perm(permissions.CREATE) + async create(@Body() dto: MaterialsInventoryDto): Promise { + await this.miService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新原材料库存' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: MaterialsInventoryUpdateDto): Promise { + await this.miService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除原材料库存' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.miService.delete(id); + } +} diff --git a/src/modules/materials_inventory/materials_inventory.dto.ts b/src/modules/materials_inventory/materials_inventory.dto.ts new file mode 100644 index 0000000..b05e466 --- /dev/null +++ b/src/modules/materials_inventory/materials_inventory.dto.ts @@ -0,0 +1,74 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { + IsArray, + IsDate, + IsDateString, + IsEnum, + IsIn, + IsInt, + IsNumber, + IsOptional, + IsString, + Matches, + MinLength +} from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { Storage } from '../tools/storage/storage.entity'; +import { Transform } from 'class-transformer'; +import dayjs from 'dayjs'; +import { formatToDate } from '~/utils'; +import { HasInventoryStatusEnum } from '~/constants/enum'; +import { DomainType } from '~/common/decorators/domain.decorator'; + +export class MaterialsInventoryDto extends DomainType {} + +export class MaterialsInventoryUpdateDto extends PartialType(MaterialsInventoryDto) {} +export class MaterialsInventoryQueryDto extends IntersectionType( + PagerDto, + PartialType(MaterialsInventoryDto), + DomainType +) { + @ApiProperty({ description: '产品名' }) + @IsOptional() + @IsString() + product: string; + + @ApiProperty({ description: '关键字' }) + @IsOptional() + @IsString() + keyword: string; + + @ApiProperty({ description: '产品名' }) + @IsOptional() + @IsEnum(HasInventoryStatusEnum) + isHasInventory: HasInventoryStatusEnum; + + @ApiProperty({ description: '项目Id' }) + @IsOptional() + @IsNumber() + projectId: number; +} +export class MaterialsInventoryExportDto extends DomainType { + @ApiProperty({ description: '项目' }) + @IsOptional() + @IsNumber() + projectId: number; + + @ApiProperty({ description: '导出时间YYYY-MM-DD' }) + @IsOptional() + @IsArray() + @Transform(params => { + // 开始和结束时间用的是一月的开始和一月的结束的时分秒 + const date = params.value; + return [ + date ? `${date[0]} 00:00:00` : null, + date ? `${date[1]} 23:59:59` : null + ]; + }) + time?: string[]; + + @ApiProperty({ description: '文件名' }) + @IsOptional() + @IsString() + filename: string; +} diff --git a/src/modules/materials_inventory/materials_inventory.entity.ts b/src/modules/materials_inventory/materials_inventory.entity.ts new file mode 100644 index 0000000..0625be9 --- /dev/null +++ b/src/modules/materials_inventory/materials_inventory.entity.ts @@ -0,0 +1,98 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { + Column, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, + Relation +} from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; +import { ProductEntity } from '../product/product.entity'; +import { ProjectEntity } from '../project/project.entity'; +import { MaterialsInOutEntity } from './in_out/materials_in_out.entity'; +import { SkDomain } from '~/common/decorators/domain.decorator'; + +@Entity({ name: 'materials_inventory' }) +export class MaterialsInventoryEntity extends CommonEntity { + @Column({ + name: 'project_id', + type: 'int', + comment: '项目' + }) + @ApiProperty({ description: '项目' }) + projectId: number; + + @Column({ + name: 'product_id', + type: 'int', + comment: '产品' + }) + @ApiProperty({ description: '产品' }) + productId: number; + + @Column({ + name: 'position', + type: 'varchar', + length: 255, + nullable: true, + comment: '库存位置' + }) + @ApiProperty({ description: '库存位置' }) + position: string; + + @Column({ + name: 'quantity', + type: 'int', + default: 0, + comment: '库存产品数量' + }) + @ApiProperty({ description: '库存产品数量' }) + quantity: number; + + @Column({ + name: 'unit_price', + type: 'decimal', + precision: 15, + default: 0, + scale: 10, + comment: '库存产品单价' + }) + @ApiProperty({ description: '库存产品单价' }) + unitPrice: number; + + @Column({ name: 'remark', type: 'varchar', length: 255, comment: '备注', nullable: true }) + @ApiProperty({ description: '备注' }) + remark: string; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; + + @Column({ type: 'int', default: 1, comment: '所属域' }) + @ApiProperty({ description: '所属域' }) + domain: SkDomain; + + @ManyToOne(() => ProjectEntity) + @JoinColumn({ name: 'project_id' }) + project: ProjectEntity; + + @ManyToOne(() => ProductEntity) + @JoinColumn({ name: 'product_id' }) + product: ProductEntity; + + @Column({ + name: 'inventory_number', + type: 'varchar', + length: 50, + comment: '库存编号' + }) + @ApiProperty({ description: '库存编号' }) + inventoryNumber: string; + + @ApiHideProperty() + @OneToMany(() => MaterialsInOutEntity, inout => inout.inventory) + materialsInOuts: Relation; +} diff --git a/src/modules/materials_inventory/materials_inventory.module.ts b/src/modules/materials_inventory/materials_inventory.module.ts new file mode 100644 index 0000000..4e9aee3 --- /dev/null +++ b/src/modules/materials_inventory/materials_inventory.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; +import { MaterialsInventoryController } from './materials_inventory.controller'; +import { MaterialsInventoryService } from './materials_inventory.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { MaterialsInventoryEntity } from './materials_inventory.entity'; +import { StorageModule } from '../tools/storage/storage.module'; +import { MaterialsInOutController } from './in_out/materials_in_out.controller'; +import { MaterialsInOutService } from './in_out/materials_in_out.service'; +import { MaterialsInOutEntity } from './in_out/materials_in_out.entity'; +import { ParamConfigModule } from '../system/param-config/param-config.module'; +import { ProjectModule } from '../project/project.module'; +import { ProjectEntity } from '../project/project.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([MaterialsInventoryEntity, MaterialsInOutEntity,ProjectEntity]), + ParamConfigModule, + StorageModule, + ProjectModule + ], + controllers: [MaterialsInventoryController, MaterialsInOutController], + providers: [MaterialsInventoryService, MaterialsInOutService] +}) +export class MaterialsInventoryModule {} diff --git a/src/modules/materials_inventory/materials_inventory.service.ts b/src/modules/materials_inventory/materials_inventory.service.ts new file mode 100644 index 0000000..a8b0516 --- /dev/null +++ b/src/modules/materials_inventory/materials_inventory.service.ts @@ -0,0 +1,571 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { MaterialsInventoryEntity } from './materials_inventory.entity'; +import { EntityManager, In, MoreThan, Repository } from 'typeorm'; +import { + MaterialsInventoryDto, + MaterialsInventoryExportDto, + MaterialsInventoryQueryDto, + MaterialsInventoryUpdateDto +} from './materials_inventory.dto'; +import { Pagination } from '~/helper/paginate/pagination'; +import { FastifyReply } from 'fastify'; +import { paginate } from '~/helper/paginate'; +import * as ExcelJS from 'exceljs'; +import dayjs from 'dayjs'; +import { MaterialsInOutEntity } from './in_out/materials_in_out.entity'; +import { fieldSearch } from '~/shared/database/field-search'; +import { groupBy, sum, uniqBy } from 'lodash'; +import { HasInventoryStatusEnum, MaterialsInOrOutEnum, ParamConfigEnum } from '~/constants/enum'; +import { ProjectEntity } from '../project/project.entity'; +import { calcNumber } from '~/utils'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { ParamConfigEntity } from '../system/param-config/param-config.entity'; +import { isDefined } from 'class-validator'; +import { DomainType } from '~/common/decorators/domain.decorator'; +@Injectable() +export class MaterialsInventoryService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(MaterialsInventoryEntity) + private materialsInventoryRepository: Repository, + @InjectRepository(MaterialsInOutEntity) + private materialsInOutRepository: Repository, + @InjectRepository(ProjectEntity) + private projectRepository: Repository, + @InjectRepository(ParamConfigEntity) + private paramConfigRepository: Repository + ) { } + + /** + * 导出原材料盘点表 + */ + async exportMaterialsInventoryCheck( + { time, projectId, domain }: MaterialsInventoryExportDto, + res: FastifyReply + ): Promise { + const ROW_HEIGHT = 20; + const HEADER_FONT_SIZE = 18; + const workbook = new ExcelJS.Workbook(); + let projects: ProjectEntity[] = []; + if (projectId) { + projects = [await this.projectRepository.findOneBy({ id: projectId })]; + } + // 查询出项目产品所属的当前库存 + const inventoriesInProjects = await this.materialsInventoryRepository.find({ + where: { + ...(projects?.length ? { projectId: In(projects.map(item => item.id)) } : null) + }, + relations: ['product', 'product.company', 'product.unit'] + }); + + // 生成数据 + const sqb = this.materialsInOutRepository + .createQueryBuilder('mio') + .leftJoin('mio.project', 'project') + .leftJoin('mio.product', 'product') + .leftJoin('product.unit', 'unit') + .leftJoin('product.company', 'company') + .addSelect([ + 'project.id', + 'project.name', + 'unit.label', + 'company.name', + 'product.name', + 'product.productSpecification', + 'product.productNumber' + ]) + .where({ + time: MoreThan(time[0]) + }) + .andWhere('mio.isDelete = 0'); + + if (projectId) { + sqb.andWhere('project.id = :projectId', { projectId }); + } + + const data = await sqb.addOrderBy('mio.time', 'DESC').getMany(); + if (!projectId) { + projects = uniqBy( + data.filter(item => item.inOrOut === MaterialsInOrOutEnum.Out).map(item => item.project), + 'id' + ); + } + + for (const project of projects) { + const currentProjectInventories = inventoriesInProjects.filter(({ projectId }) => + Object.is(projectId, project.id) + ); + const currentProjectData = data.filter( + item => item.projectId === project.id || item.inOrOut === MaterialsInOrOutEnum.Out + ); + const currentMonthProjectData = currentProjectData.filter(item => { + return ( + dayjs(item.time).isAfter(dayjs(time[0])) && dayjs(item.time).isBefore(dayjs(time[1])) + ); + }); + const sheet = workbook.addWorksheet(project.name); + sheet.mergeCells('A1:T1'); + // 设置标题 + sheet.getCell('A1').value = '山东矿机华信智能科技有限公司原材料盘点表'; + // 设置日期 + sheet.mergeCells('A2:B2'); + sheet.getCell('A2').value = `日期:${dayjs(time[0]).format('YYYY年M月')}`; + // 设置表头 + const headers = [ + '序号', + '公司名称', + '产品名称', + '单位', + '库存数量', + '单价', + '金额', + '', + '', + '', + '', + '', + '', + '', + '', + '结存数量', + '单价', + '金额', + '备注' + ]; + sheet.addRow(headers); + sheet.addRow([ + '', + '', + '', + '', + '', + '', + '', + '入库时间', + '数量', + '单价', + '金额', + '出库时间', + '数量', + '单价', + '金额', + '', + '', + '', + '' + ]); + for (let i = 1; i <= 7; i++) { + sheet.mergeCells(`${String.fromCharCode(64 + i)}3:${String.fromCharCode(64 + i)}4`); + } + // 入库 + sheet.mergeCells('H3:K3'); + sheet.getCell('H3').value = '入库'; + + // 出库 + sheet.mergeCells('L3:O3'); + sheet.getCell('L3').value = '出库'; + + for (let i = 8; i <= 15; i++) { + sheet.getCell(`${String.fromCharCode(64 + i)}4`).style.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFFFC000' } + }; + } + + for (let i = 16; i <= 19; i++) { + sheet.mergeCells(`${String.fromCharCode(64 + i)}3:${String.fromCharCode(64 + i)}4`); + } + + // 固定信息样式设定 + sheet.eachRow((row, index) => { + row.alignment = { vertical: 'middle', horizontal: 'center' }; + row.font = { bold: true }; + row.height = ROW_HEIGHT; + if (index >= 3) { + row.eachCell(cell => { + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + }; + }); + } + }); + + // 根据库存Id分组 + const groupedData = groupBy( + currentMonthProjectData, + record => record.inventoryId + ); + let number = 0; + const groupedInventories = groupBy(currentProjectInventories, item => item.id); + let orderNo = 0; + + for (const key in groupedInventories) { + orderNo++; + // 目前暂定逻辑出库只有一次或者没有出库。不会对一个入库的记录多次出库,故而用find。---废弃 + // 2024.04.16 改成 + const inventory = groupedInventories[key][0]; + const outRecords = groupedData[key].filter( + item => item.inOrOut === MaterialsInOrOutEnum.Out + ); + const inRecords = groupedData[key].filter(item => item.inOrOut === MaterialsInOrOutEnum.In); + const outRecordQuantity = outRecords + .map(item => item.quantity) + .reduce((acc, cur) => { + return calcNumber(acc, cur, 'add'); + }, 0); + + const inRecordQuantity = inRecords + .map(item => item.quantity) + .reduce((acc, cur) => { + return calcNumber(acc, cur, 'add'); + }, 0); + // 这里的单价默认入库价格和出库价格一致,所以直接用总数量*入库单价 + const outRecordAmount = calcNumber(outRecordQuantity, inventory.unitPrice || 0, 'multiply'); + const inRecordAmount = calcNumber(inRecordQuantity, inventory.unitPrice || 0, 'multiply'); + const currInventories = groupedInventories[key]?.shift(); + const allDataFromMonth = data.filter(res => res.inventoryId == Number(key)); + let currentQuantity = 0; + let balanceQuantity = 0; + // 月初库存数量 + if (currInventories) { + const sumIn = sum( + allDataFromMonth + .filter(res => Object.is(res.inOrOut, MaterialsInOrOutEnum.In)) + .map(item => item.quantity) + ); + const sumOut = sum( + allDataFromMonth + .filter(res => Object.is(res.inOrOut, MaterialsInOrOutEnum.Out)) + .map(item => item.quantity) + ); + const sumDistance = calcNumber(sumIn, sumOut, 'subtract'); + currentQuantity = calcNumber(currInventories.quantity, sumDistance, 'subtract'); + } + // 结存库存数量 + balanceQuantity = calcNumber( + currentQuantity, + calcNumber(inRecordQuantity, outRecordQuantity, 'subtract'), + 'add' + ); + number++; + sheet.addRow([ + `${orderNo}`, + inventory.product?.company?.name || '', + inventory.product?.name || '', + inventory.product.unit.label || '', + currentQuantity, + parseFloat(`${inventory.unitPrice || 0}`), + calcNumber(currentQuantity, inventory.unitPrice || 0, 'multiply'), + // inRecord.time, + '', + inRecordQuantity, + parseFloat(`${inventory.unitPrice || 0}`), + parseFloat(`${inRecordAmount}`), + // outRecord?.time || '', + '', + outRecordQuantity, + parseFloat(`${inventory?.unitPrice || 0}`), + parseFloat(`${outRecordAmount}`), + balanceQuantity, + parseFloat(`${inventory?.unitPrice || 0}`), + calcNumber(balanceQuantity, inventory?.unitPrice || 0, 'multiply'), + // `${inRecord?.agent || ''}/${outRecord?.agent || ''}`, + '' + ]); + } + sheet.getCell('A1').font = { size: HEADER_FONT_SIZE }; + + // 固定信息样式设定 + sheet.eachRow((row, index) => { + if (index >= 5) { + row.alignment = { vertical: 'middle', horizontal: 'center' }; + row.height = ROW_HEIGHT; + row.eachCell(cell => { + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + }; + }); + } + }); + + sheet.columns.forEach((column, index: number) => { + let maxColumnLength = 0; + const autoWidth = ['B', 'C', 'S', 'U']; + if (String.fromCharCode(65 + index) === 'B') maxColumnLength = 20; + if (autoWidth.includes(String.fromCharCode(65 + index))) { + column.eachCell({ includeEmpty: true }, (cell, rowIndex) => { + if (rowIndex >= 5) { + const columnLength = `${cell.value || ''}`.length; + if (columnLength > maxColumnLength) { + maxColumnLength = columnLength; + } + } + }); + column.width = maxColumnLength < 12 ? 12 : maxColumnLength; // Minimum width of 10 + } else { + column.width = 12; + } + }); + } + //读取buffer进行传输 + const buffer = await workbook.xlsx.writeBuffer(); + res + .header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + .header( + 'Content-Disposition', + `attachment; filename="${encodeURIComponent('导出_excel' + new Date().getTime() + '.xls')}"` + ) + .send(buffer); + } + + /** + * 查询所有盘点信息 + */ + async findAll({ + page, + pageSize, + product, + keyword, + projectId, + isHasInventory, + domain + }: MaterialsInventoryQueryDto): Promise> { + const queryBuilder = this.materialsInventoryRepository + .createQueryBuilder('materialsInventory') + .leftJoin('materialsInventory.project', 'project') + .leftJoin('materialsInventory.product', 'product') + .leftJoin('product.unit', 'unit') + .leftJoin('product.company', 'company') + .addSelect([ + 'project.name', + 'project.id', + 'unit.id', + 'unit.label', + 'company.id', + 'company.name', + 'product.id', + 'product.name', + 'product.productSpecification', + 'product.productNumber' + ]) + .where(fieldSearch({ domain })) + .andWhere('materialsInventory.isDelete = 0'); + if (product) { + queryBuilder.andWhere('product.name like :product', { product: `%${product}%` }); + } + + if (projectId) { + queryBuilder.andWhere('project.id = :projectId', { projectId }); + } + + if (keyword) { + queryBuilder.andWhere( + '(materialsInventory.inventoryNumber like :keyword or product.name like :keyword or product.productNumber like :keyword or product.productSpecification like :keyword)', + { + keyword: `%${keyword}%` + } + ); + } + if (isHasInventory == HasInventoryStatusEnum.Yes) { + queryBuilder.andWhere('materialsInventory.quantity > 0'); + } + if (isHasInventory == HasInventoryStatusEnum.No) { + queryBuilder.andWhere('materialsInventory.quantity = 0'); + } + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增库存 + */ + async create(dto: MaterialsInventoryDto): Promise { + await this.materialsInventoryRepository.insert(dto); + } + + /** + * 更新库存 + */ + async update(id: number, data: Partial): Promise { + await this.entityManager.transaction(async manager => { + await manager.update(MaterialsInventoryEntity, id, { + ...data + }); + }); + } + + /** + * 产品入库后计算最新库存 + * 请注意。产品库存需要根据产品id和价格双主键存储。因为产品价格会变化,需要分开统计。 + * @param data 传入项目ID,产品ID和入库数量和单价 + * @param manager 传入事务对象防止开启多重事务 + */ + async inInventory( + data: { + position?: string; + projectId: number; + productId: number; + quantity: number; + inventoryId?: number; + unitPrice?: number; + changedUnitPrice?: number; + }, + manager: EntityManager, + domain?: DomainType + ): Promise { + const { + projectId, + productId, + quantity: inQuantity, + unitPrice, + changedUnitPrice, + position, + inventoryId + } = data; + let searchPayload: any = {}; + if (isDefined(inventoryId)) { + searchPayload = { id: inventoryId, domain }; + } else { + searchPayload = { projectId, productId, unitPrice, domain }; + } + const exsitedInventory = await manager.findOne(MaterialsInventoryEntity, { + where: searchPayload, // 根据项目,产品,价格查出之前的实时库存情况 + lock: { mode: 'pessimistic_write' } // 开启悲观行锁,防止脏读和修改 + }); + + // 若不存在库存,直接新增库存 + if (!exsitedInventory) { + const inventoryNumber = await this.generateInventoryNumber(); + const { raw } = await manager.insert(MaterialsInventoryEntity, { + projectId, + productId, + unitPrice, + inventoryNumber, + position, + quantity: inQuantity + }); + return manager.findOne(MaterialsInventoryEntity, { where: { id: raw.insertId } }); + } + // 若该项目存在库存,则该项目该产品的库存增加 + let { quantity, id } = exsitedInventory; + const newQuantity = calcNumber(quantity || 0, inQuantity || 0, 'add'); + if (isNaN(newQuantity)) { + throw new Error('库存数量不合法'); + } + await manager.update(MaterialsInventoryEntity, id, { + quantity: newQuantity, + unitPrice: changedUnitPrice || undefined + }); + + return manager.findOne(MaterialsInventoryEntity, { where: { id } }); + } + + /** + * 产品出库 + * @param data 传入库存ID(一定存在。) + * @param manager 传入事务对象防止开启多重事务 + */ + async outInventory( + data: { + quantity: number; + inventoryId?: number; + }, + manager: EntityManager, + ): Promise { + const { quantity: outQuantity, inventoryId } = data; + // 开启悲观行锁,防止脏读和修改 + const inventory = await manager.findOne(MaterialsInventoryEntity, { + where: { id: inventoryId }, + lock: { mode: 'pessimistic_write' } + }); + // 检查库存剩余 + if (inventory.quantity < outQuantity) { + throw new BusinessException(ErrorEnum.INVENTORY_INSUFFICIENT); + } + // 若该项目的该产品库存充足,则该项目该产品的库存减少 + let { quantity, id } = inventory; + const newQuantity = calcNumber(quantity || 0, outQuantity || 0, 'subtract'); + if (isNaN(newQuantity)) { + throw new BusinessException(ErrorEnum.INVENTORY_INSUFFICIENT); + } + await manager.update(MaterialsInventoryEntity, id, { + quantity: newQuantity + }); + + return manager.findOne(MaterialsInventoryEntity, { where: { id } }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + // 比较重要,做逻辑删除 + await this.materialsInventoryRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取某个价格的某个商品库存信息 + */ + async info(id: number) { + const info = await this.materialsInventoryRepository + .createQueryBuilder('materialsInventory') + .leftJoin('materialsInventory.project', 'project') + .leftJoin('materialsInventory.product', 'product') + .leftJoin('product.unit', 'unit') + .leftJoin('product.company', 'company') + .addSelect([ + 'project.name', + 'project.id', + 'product.id', + 'product.name', + 'unit.label', + 'company.name', + 'product.productSpecification', + 'product.productNumber' + ]) + .where({ + id + }) + .andWhere('materialsInventory.isDelete = 0') + .getOne(); + return info; + } + + /** + * 生成库存编号 + * @returns 库存编号 + */ + async generateInventoryNumber() { + const prefix = + ( + await this.paramConfigRepository.findOne({ + where: { + key: ParamConfigEnum.InventoryNumberPrefix + } + }) + )?.value || ''; + const lastInventory = await this.materialsInventoryRepository + .createQueryBuilder('materials_inventory') + .select( + `MAX(CAST(REPLACE(materials_inventory.inventoryNumber, '${prefix}', '') AS UNSIGNED))`, + 'maxInventoryNumber' + ) + .getRawOne(); + const lastNumber = lastInventory.maxInventoryNumber + ? parseInt(lastInventory.maxInventoryNumber.replace(prefix, '')) + : 0; + const newNumber = lastNumber + 1 < 1000 ? 1000 : lastNumber + 1; + return `${prefix}${newNumber}`; + } +} diff --git a/src/modules/netdisk/manager/manage-qiniu.service.ts b/src/modules/netdisk/manager/manage-qiniu.service.ts new file mode 100644 index 0000000..3554b7b --- /dev/null +++ b/src/modules/netdisk/manager/manage-qiniu.service.ts @@ -0,0 +1,836 @@ +import { basename, extname } from 'node:path'; + +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { isEmpty } from 'lodash'; +import * as qiniu from 'qiniu'; +import { auth, conf, rs } from 'qiniu'; + +import { ConfigKeyPaths } from '~/config'; +import { + NETDISK_COPY_SUFFIX, + NETDISK_DELIMITER, + NETDISK_HANDLE_MAX_ITEM, + NETDISK_LIMIT +} from '~/constants/oss.constant'; + +import { AccountInfo } from '~/modules/user/user.model'; +import { UserService } from '~/modules/user/user.service'; + +import { generateRandomValue } from '~/utils'; + +import { SFileInfo, SFileInfoDetail, SFileList } from './manage.class'; +import { FileOpItem } from './manage.dto'; + +@Injectable() +export class QiNiuNetDiskManageService { + private config: conf.ConfigOptions; + private mac: auth.digest.Mac; + private bucketManager: rs.BucketManager; + + private get qiniuConfig() { + return this.configService.get('oss', { infer: true }); + } + + constructor( + private configService: ConfigService, + private userService: UserService + ) { + this.mac = new qiniu.auth.digest.Mac(this.qiniuConfig.accessKey, this.qiniuConfig.secretKey); + this.config = new qiniu.conf.Config({ + zone: this.qiniuConfig.zone + }); + // bucket manager + this.bucketManager = new qiniu.rs.BucketManager(this.mac, this.config); + } + + /** + * 获取文件列表 + * @param prefix 当前文件夹路径,搜索模式下会被忽略 + * @param marker 下一页标识 + * @returns iFileListResult + */ + async getFileList(prefix = '', marker = '', skey = ''): Promise { + // 是否需要搜索 + const searching = !isEmpty(skey); + return new Promise((resolve, reject) => { + this.bucketManager.listPrefix( + this.qiniuConfig.bucket, + { + prefix: searching ? '' : prefix, + limit: NETDISK_LIMIT, + delimiter: searching ? '' : NETDISK_DELIMITER, + marker + }, + (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + // 如果这个nextMarker不为空,那么还有未列举完毕的文件列表,下次调用listPrefix的时候, + // 指定options里面的marker为这个值 + const fileList: SFileInfo[] = []; + // 处理目录,但只有非搜索模式下可用 + if (!searching && !isEmpty(respBody.commonPrefixes)) { + // dir + for (const dirPath of respBody.commonPrefixes) { + const name = (dirPath as string).substr(0, dirPath.length - 1).replace(prefix, ''); + if (isEmpty(skey) || name.includes(skey)) { + fileList.push({ + name: (dirPath as string).substr(0, dirPath.length - 1).replace(prefix, ''), + type: 'dir', + id: generateRandomValue(10) + }); + } + } + } + // handle items + if (!isEmpty(respBody.items)) { + // file + for (const item of respBody.items) { + // 搜索模式下处理 + if (searching) { + const pathList: string[] = item.key.split(NETDISK_DELIMITER); + // dir is empty stirng, file is key string + const name = pathList.pop(); + if ( + item.key.endsWith(NETDISK_DELIMITER) && + pathList[pathList.length - 1].includes(skey) + ) { + // 结果是目录 + const ditName = pathList.pop(); + fileList.push({ + id: generateRandomValue(10), + name: ditName, + type: 'dir', + belongTo: pathList.join(NETDISK_DELIMITER) + }); + } else if (name.includes(skey)) { + // 文件 + fileList.push({ + id: generateRandomValue(10), + name, + type: 'file', + fsize: item.fsize, + mimeType: item.mimeType, + putTime: new Date(Number.parseInt(item.putTime) / 10000), + belongTo: pathList.join(NETDISK_DELIMITER) + }); + } + } else { + // 正常获取列表 + const fileKey = item.key.replace(prefix, '') as string; + if (!isEmpty(fileKey)) { + fileList.push({ + id: generateRandomValue(10), + name: fileKey, + type: 'file', + fsize: item.fsize, + mimeType: item.mimeType, + putTime: new Date(Number.parseInt(item.putTime) / 10000) + }); + } + } + } + } + resolve({ + list: fileList, + marker: respBody.marker || null + }); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + } + ); + }); + } + + /** + * 获取文件信息 + */ + async getFileInfo(name: string, path: string): Promise { + return new Promise((resolve, reject) => { + this.bucketManager.stat( + this.qiniuConfig.bucket, + `${path}${name}`, + (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + const detailInfo: SFileInfoDetail = { + fsize: respBody.fsize, + hash: respBody.hash, + md5: respBody.md5, + mimeType: respBody.mimeType.split('/x-qn-meta')[0], + putTime: new Date(Number.parseInt(respBody.putTime) / 10000), + type: respBody.type, + uploader: '', + mark: respBody?.['x-qn-meta']?.['!mark'] ?? '' + }; + if (!respBody.endUser) { + resolve(detailInfo); + } else { + this.userService + .getAccountInfo(Number.parseInt(respBody.endUser)) + .then((user: AccountInfo) => { + if (isEmpty(user)) { + resolve(detailInfo); + } else { + detailInfo.uploader = user.username; + resolve(detailInfo); + } + }); + } + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + } + ); + }); + } + + /** + * 修改文件MimeType + */ + async changeFileHeaders( + name: string, + path: string, + headers: { [k: string]: string } + ): Promise { + return new Promise((resolve, reject) => { + this.bucketManager.changeHeaders( + this.qiniuConfig.bucket, + `${path}${name}`, + headers, + (err, _, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + resolve(); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + } + ); + }); + } + + /** + * 创建文件夹 + * @returns true创建成功 + */ + async createDir(dirName: string): Promise { + const safeDirName = dirName.endsWith('/') ? dirName : `${dirName}/`; + return new Promise((resolve, reject) => { + // 上传一个空文件以用于显示文件夹效果 + const formUploader = new qiniu.form_up.FormUploader(this.config); + const putExtra = new qiniu.form_up.PutExtra(); + formUploader.put( + this.createUploadToken(''), + safeDirName, + ' ', + putExtra, + (respErr, respBody, respInfo) => { + if (respErr) { + reject(respErr); + return; + } + if (respInfo.statusCode === 200) { + resolve(); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + } + ); + }); + } + + /** + * 检查文件是否存在,同可检查目录 + */ + async checkFileExist(filePath: string): Promise { + return new Promise((resolve, reject) => { + // fix path end must a / + + // 检测文件夹是否存在 + this.bucketManager.stat(this.qiniuConfig.bucket, filePath, (respErr, respBody, respInfo) => { + if (respErr) { + reject(respErr); + return; + } + if (respInfo.statusCode === 200) { + // 文件夹存在 + resolve(true); + } else if (respInfo.statusCode === 612) { + // 文件夹不存在 + resolve(false); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + }); + }); + } + + /** + * 创建Upload Token, 默认过期时间一小时 + * @returns upload token + */ + createUploadToken(endUser: string): string { + const policy = new qiniu.rs.PutPolicy({ + scope: this.qiniuConfig.bucket, + insertOnly: 1, + fsizeLimit: 1024 ** 2 * 10, + endUser + }); + const uploadToken = policy.uploadToken(this.mac); + return uploadToken; + } + + /** + * 重命名文件 + * @param dir 文件路径 + * @param name 文件名称 + */ + async renameFile(dir: string, name: string, toName: string): Promise { + const fileName = `${dir}${name}`; + const toFileName = `${dir}${toName}`; + const op = { + force: true + }; + return new Promise((resolve, reject) => { + this.bucketManager.move( + this.qiniuConfig.bucket, + fileName, + this.qiniuConfig.bucket, + toFileName, + op, + (err, respBody, respInfo) => { + if (err) { + reject(err); + } else { + if (respInfo.statusCode === 200) { + resolve(); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + ) + ); + } + } + } + ); + }); + } + + /** + * 移动文件 + */ + async moveFile(dir: string, toDir: string, name: string): Promise { + const fileName = `${dir}${name}`; + const toFileName = `${toDir}${name}`; + const op = { + force: true + }; + return new Promise((resolve, reject) => { + this.bucketManager.move( + this.qiniuConfig.bucket, + fileName, + this.qiniuConfig.bucket, + toFileName, + op, + (err, respBody, respInfo) => { + if (err) { + reject(err); + } else { + if (respInfo.statusCode === 200) { + resolve(); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + ) + ); + } + } + } + ); + }); + } + + /** + * 复制文件 + */ + async copyFile(dir: string, toDir: string, name: string): Promise { + const fileName = `${dir}${name}`; + // 拼接文件名 + const ext = extname(name); + const bn = basename(name, ext); + const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`; + const op = { + force: true + }; + return new Promise((resolve, reject) => { + this.bucketManager.copy( + this.qiniuConfig.bucket, + fileName, + this.qiniuConfig.bucket, + toFileName, + op, + (err, respBody, respInfo) => { + if (err) { + reject(err); + } else { + if (respInfo.statusCode === 200) { + resolve(); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + ) + ); + } + } + } + ); + }); + } + + /** + * 重命名文件夹 + */ + async renameDir(path: string, name: string, toName: string): Promise { + const dirName = `${path}${name}`; + const toDirName = `${path}${toName}`; + let hasFile = true; + let marker = ''; + const op = { + force: true + }; + const bucketName = this.qiniuConfig.bucket; + while (hasFile) { + await new Promise((resolve, reject) => { + // 列举当前目录下的所有文件 + this.bucketManager.listPrefix( + this.qiniuConfig.bucket, + { + prefix: dirName, + limit: NETDISK_HANDLE_MAX_ITEM, + marker + }, + (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + const moveOperations = respBody.items.map(item => { + const { key } = item; + const destKey = key.replace(dirName, toDirName); + return qiniu.rs.moveOp(bucketName, key, bucketName, destKey, op); + }); + this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + if (err2) { + reject(err2); + return; + } + if (respInfo2.statusCode === 200) { + if (isEmpty(respBody.marker)) hasFile = false; + else marker = respBody.marker; + + resolve(); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + ) + ); + } + }); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + ) + ); + } + } + ); + }); + } + } + + /** + * 获取七牛下载的文件url链接 + * @param key 文件路径 + * @returns 连接 + */ + getDownloadLink(key: string): string { + if (this.qiniuConfig.access === 'public') { + return this.bucketManager.publicDownloadUrl(this.qiniuConfig.domain, key); + } else if (this.qiniuConfig.access === 'private') { + return this.bucketManager.privateDownloadUrl( + this.qiniuConfig.domain, + key, + Date.now() / 1000 + 36000 + ); + } + throw new Error('qiniu config access type not support'); + } + + /** + * 删除文件 + * @param dir 删除的文件夹目录 + * @param name 文件名 + */ + async deleteFile(dir: string, name: string): Promise { + return new Promise((resolve, reject) => { + this.bucketManager.delete( + this.qiniuConfig.bucket, + `${dir}${name}`, + (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + resolve(); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + } + ); + }); + } + + /** + * 删除文件夹 + * @param dir 文件夹所在的上级目录 + * @param name 文件目录名称 + */ + async deleteMultiFileOrDir(fileList: FileOpItem[], dir: string): Promise { + const files = fileList.filter(item => item.type === 'file'); + if (files.length > 0) { + // 批处理文件 + const copyOperations = files.map(item => { + const fileName = `${dir}${item.name}`; + return qiniu.rs.deleteOp(this.qiniuConfig.bucket, fileName); + }); + await new Promise((resolve, reject) => { + this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + resolve(); + } else if (respInfo.statusCode === 298) { + reject(new Error('操作异常,但部分文件夹删除成功')); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + }); + }); + } + // 处理文件夹 + const dirs = fileList.filter(item => item.type === 'dir'); + if (dirs.length > 0) { + // 处理文件夹的复制 + for (let i = 0; i < dirs.length; i++) { + const dirName = `${dir}${dirs[i].name}/`; + let hasFile = true; + let marker = ''; + while (hasFile) { + await new Promise((resolve, reject) => { + // 列举当前目录下的所有文件 + this.bucketManager.listPrefix( + this.qiniuConfig.bucket, + { + prefix: dirName, + limit: NETDISK_HANDLE_MAX_ITEM, + marker + }, + (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + const moveOperations = respBody.items.map(item => { + const { key } = item; + return qiniu.rs.deleteOp(this.qiniuConfig.bucket, key); + }); + this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + if (err2) { + reject(err2); + return; + } + if (respInfo2.statusCode === 200) { + if (isEmpty(respBody.marker)) hasFile = false; + else marker = respBody.marker; + + resolve(); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + ) + ); + } + }); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + ) + ); + } + } + ); + }); + } + } + } + } + + /** + * 复制文件,含文件夹 + */ + async copyMultiFileOrDir(fileList: FileOpItem[], dir: string, toDir: string): Promise { + const files = fileList.filter(item => item.type === 'file'); + const op = { + force: true + }; + if (files.length > 0) { + // 批处理文件 + const copyOperations = files.map(item => { + const fileName = `${dir}${item.name}`; + // 拼接文件名 + const ext = extname(item.name); + const bn = basename(item.name, ext); + const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`; + return qiniu.rs.copyOp( + this.qiniuConfig.bucket, + fileName, + this.qiniuConfig.bucket, + toFileName, + op + ); + }); + await new Promise((resolve, reject) => { + this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + resolve(); + } else if (respInfo.statusCode === 298) { + reject(new Error('操作异常,但部分文件夹删除成功')); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + }); + }); + } + // 处理文件夹 + const dirs = fileList.filter(item => item.type === 'dir'); + if (dirs.length > 0) { + // 处理文件夹的复制 + for (let i = 0; i < dirs.length; i++) { + const dirName = `${dir}${dirs[i].name}/`; + const copyDirName = `${toDir}${dirs[i].name}${NETDISK_COPY_SUFFIX}/`; + let hasFile = true; + let marker = ''; + while (hasFile) { + await new Promise((resolve, reject) => { + // 列举当前目录下的所有文件 + this.bucketManager.listPrefix( + this.qiniuConfig.bucket, + { + prefix: dirName, + limit: NETDISK_HANDLE_MAX_ITEM, + marker + }, + (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + const moveOperations = respBody.items.map(item => { + const { key } = item; + const destKey = key.replace(dirName, copyDirName); + return qiniu.rs.copyOp( + this.qiniuConfig.bucket, + key, + this.qiniuConfig.bucket, + destKey, + op + ); + }); + this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + if (err2) { + reject(err2); + return; + } + if (respInfo2.statusCode === 200) { + if (isEmpty(respBody.marker)) hasFile = false; + else marker = respBody.marker; + + resolve(); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + ) + ); + } + }); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + ) + ); + } + } + ); + }); + } + } + } + } + + /** + * 移动文件,含文件夹 + */ + async moveMultiFileOrDir(fileList: FileOpItem[], dir: string, toDir: string): Promise { + const files = fileList.filter(item => item.type === 'file'); + const op = { + force: true + }; + if (files.length > 0) { + // 批处理文件 + const copyOperations = files.map(item => { + const fileName = `${dir}${item.name}`; + const toFileName = `${toDir}${item.name}`; + return qiniu.rs.moveOp( + this.qiniuConfig.bucket, + fileName, + this.qiniuConfig.bucket, + toFileName, + op + ); + }); + await new Promise((resolve, reject) => { + this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + resolve(); + } else if (respInfo.statusCode === 298) { + reject(new Error('操作异常,但部分文件夹删除成功')); + } else { + reject( + new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + ); + } + }); + }); + } + // 处理文件夹 + const dirs = fileList.filter(item => item.type === 'dir'); + if (dirs.length > 0) { + // 处理文件夹的复制 + for (let i = 0; i < dirs.length; i++) { + const dirName = `${dir}${dirs[i].name}/`; + const toDirName = `${toDir}${dirs[i].name}/`; + // 移动的目录不是是自己 + if (toDirName.startsWith(dirName)) continue; + + let hasFile = true; + let marker = ''; + while (hasFile) { + await new Promise((resolve, reject) => { + // 列举当前目录下的所有文件 + this.bucketManager.listPrefix( + this.qiniuConfig.bucket, + { + prefix: dirName, + limit: NETDISK_HANDLE_MAX_ITEM, + marker + }, + (err, respBody, respInfo) => { + if (err) { + reject(err); + return; + } + if (respInfo.statusCode === 200) { + const moveOperations = respBody.items.map(item => { + const { key } = item; + const destKey = key.replace(dirName, toDirName); + return qiniu.rs.moveOp( + this.qiniuConfig.bucket, + key, + this.qiniuConfig.bucket, + destKey, + op + ); + }); + this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + if (err2) { + reject(err2); + return; + } + if (respInfo2.statusCode === 200) { + if (isEmpty(respBody.marker)) hasFile = false; + else marker = respBody.marker; + + resolve(); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + ) + ); + } + }); + } else { + reject( + new Error( + `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + ) + ); + } + } + ); + }); + } + } + } + } +} diff --git a/src/modules/netdisk/manager/manage.class.ts b/src/modules/netdisk/manager/manage.class.ts new file mode 100644 index 0000000..3f236f6 --- /dev/null +++ b/src/modules/netdisk/manager/manage.class.ts @@ -0,0 +1,67 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export type FileType = 'file' | 'dir'; + +export class SFileInfo { + @ApiProperty({ description: '文件id' }) + id: string; + + @ApiProperty({ description: '文件类型', enum: ['file', 'dir'] }) + type: FileType; + + @ApiProperty({ description: '文件名称' }) + name: string; + + @ApiProperty({ description: '存入时间', type: Date }) + putTime?: Date; + + @ApiProperty({ description: '文件大小, byte单位' }) + fsize?: string; + + @ApiProperty({ description: '文件的mime-type' }) + mimeType?: string; + + @ApiProperty({ description: '所属目录' }) + belongTo?: string; +} + +export class SFileList { + @ApiProperty({ description: '文件列表', type: [SFileInfo] }) + list: SFileInfo[]; + + @ApiProperty({ description: '分页标志,空则代表加载完毕' }) + marker?: string; +} + +export class UploadToken { + @ApiProperty({ description: '上传token' }) + token: string; +} + +export class SFileInfoDetail { + @ApiProperty({ description: '文件大小,int64类型,单位为字节(Byte)' }) + fsize: number; + + @ApiProperty({ description: '文件HASH值' }) + hash: string; + + @ApiProperty({ description: '文件MIME类型,string类型' }) + mimeType: string; + + @ApiProperty({ + description: '文件存储类型,2 表示归档存储,1 表示低频存储,0表示普通存储。' + }) + type?: number; + + @ApiProperty({ description: '文件上传时间', type: Date }) + putTime: Date; + + @ApiProperty({ description: '文件md5值' }) + md5: string; + + @ApiProperty({ description: '上传人' }) + uploader?: string; + + @ApiProperty({ description: '文件备注' }) + mark?: string; +} diff --git a/src/modules/netdisk/manager/manage.controller.ts b/src/modules/netdisk/manager/manage.controller.ts new file mode 100644 index 0000000..f3e092c --- /dev/null +++ b/src/modules/netdisk/manager/manage.controller.ts @@ -0,0 +1,135 @@ +import { Body, Controller, Get, Post, Query } from '@nestjs/common'; +import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { AuthUser } from '~/modules/auth/decorators/auth-user.decorator'; + +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; + +import { checkIsDemoMode } from '~/utils'; + +import { SFileInfoDetail, SFileList, UploadToken } from './manage.class'; +import { + DeleteDto, + FileInfoDto, + FileOpDto, + GetFileListDto, + MKDirDto, + MarkFileDto, + RenameDto +} from './manage.dto'; +import { NetDiskManageService } from './manage.service'; + +export const permissions = definePermission('netdisk:manage', { + LIST: 'list', + CREATE: 'create', + INFO: 'info', + UPDATE: 'update', + DELETE: 'delete', + MKDIR: 'mkdir', + TOKEN: 'token', + MARK: 'mark', + DOWNLOAD: 'download', + RENAME: 'rename', + CUT: 'cut', + COPY: 'copy' +} as const); + +@ApiTags('NetDiskManage - 网盘管理模块') +@Controller('manage') +export class NetDiskManageController { + constructor(private manageService: NetDiskManageService) {} + + @Get('list') + @ApiOperation({ summary: '获取文件列表' }) + @ApiOkResponse({ type: SFileList }) + @Perm(permissions.LIST) + async list(@Query() dto: GetFileListDto): Promise { + return await this.manageService.getFileList(dto.path, dto.marker, dto.key); + } + + // @Post('mkdir') + // @ApiOperation({ summary: '创建文件夹,支持多级' }) + // @Perm(permissions.MKDIR) + // async mkdir(@Body() dto: MKDirDto): Promise { + // const result = await this.manageService.checkFileExist(`${dto.path}${dto.dirName}/`); + // if (result) throw new BusinessException(ErrorEnum.OSS_FILE_OR_DIR_EXIST); + + // await this.manageService.createDir(`${dto.path}${dto.dirName}`); + // } + + // @Get('token') + // @ApiOperation({ summary: '获取上传Token,无Token前端无法上传' }) + // @ApiOkResponse({ type: UploadToken }) + // @Perm(permissions.TOKEN) + // async token(@AuthUser() user: IAuthUser): Promise { + // checkIsDemoMode(); + + // return { + // token: this.manageService.createUploadToken(`${user.uid}`) + // }; + // } + + @Get('info') + @ApiOperation({ summary: '获取文件详细信息' }) + @ApiOkResponse({ type: SFileInfoDetail }) + @Perm(permissions.INFO) + async info(@Query() dto: FileInfoDto): Promise { + return await this.manageService.getFileInfo(dto.name, dto.path); + } + + // @Post('mark') + // @ApiOperation({ summary: '添加文件备注' }) + // @Perm(permissions.MARK) + // async mark(@Body() dto: MarkFileDto): Promise { + // await this.manageService.changeFileHeaders(dto.name, dto.path, { + // mark: dto.mark + // }); + // } + + @Get('download') + @ApiOperation({ summary: '获取下载链接,不支持下载文件夹' }) + @ApiOkResponse({ type: String }) + @Perm(permissions.DOWNLOAD) + async download(@Query() dto: FileInfoDto): Promise { + return this.manageService.getDownloadLink(`${dto.path}${dto.name}`); + } + + // @Post('rename') + // @ApiOperation({ summary: '重命名文件或文件夹' }) + // @Perm(permissions.RENAME) + // async rename(@Body() dto: RenameDto): Promise { + // const result = await this.manageService.checkFileExist( + // `${dto.path}${dto.toName}${dto.type === 'dir' ? '/' : ''}` + // ); + // if (result) throw new BusinessException(ErrorEnum.OSS_FILE_OR_DIR_EXIST); + + // if (dto.type === 'file') await this.manageService.renameFile(dto.path, dto.name, dto.toName); + // else await this.manageService.renameDir(dto.path, dto.name, dto.toName); + // } + + // @Post('delete') + // @ApiOperation({ summary: '删除文件或文件夹' }) + // @Perm(permissions.DELETE) + // async delete(@Body() dto: DeleteDto): Promise { + // await this.manageService.deleteMultiFileOrDir(dto.files, dto.path); + // } + + // @Post('cut') + // @ApiOperation({ summary: '剪切文件或文件夹,支持批量' }) + // @Perm(permissions.CUT) + // async cut(@Body() dto: FileOpDto): Promise { + // if (dto.originPath === dto.toPath) + // throw new BusinessException(ErrorEnum.OSS_NO_OPERATION_REQUIRED); + + // await this.manageService.moveMultiFileOrDir(dto.files, dto.originPath, dto.toPath); + // } + + // @Post('copy') + // @ApiOperation({ summary: '复制文件或文件夹,支持批量' }) + // @Perm(permissions.COPY) + // async copy(@Body() dto: FileOpDto): Promise { + // await this.manageService.copyMultiFileOrDir(dto.files, dto.originPath, dto.toPath); + // } +} diff --git a/src/modules/netdisk/manager/manage.dto.ts b/src/modules/netdisk/manager/manage.dto.ts new file mode 100644 index 0000000..11897e4 --- /dev/null +++ b/src/modules/netdisk/manager/manage.dto.ts @@ -0,0 +1,159 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + ArrayMaxSize, + IsNotEmpty, + IsOptional, + IsString, + Matches, + Validate, + ValidateIf, + ValidateNested, + ValidationArguments, + ValidatorConstraint, + ValidatorConstraintInterface +} from 'class-validator'; +import { isEmpty } from 'lodash'; + +import { NETDISK_HANDLE_MAX_ITEM } from '~/constants/oss.constant'; + +@ValidatorConstraint({ name: 'IsLegalNameExpression', async: false }) +export class IsLegalNameExpression implements ValidatorConstraintInterface { + validate(value: string, args: ValidationArguments) { + try { + if (isEmpty(value)) throw new Error('dir name is empty'); + + if (value.includes('/')) throw new Error('dir name not allow /'); + + return true; + } catch (e) { + return false; + } + } + + defaultMessage(_args: ValidationArguments) { + // here you can provide default error message if validation failed + return 'file or dir name invalid'; + } +} + +export class FileOpItem { + @ApiProperty({ description: '文件类型', enum: ['file', 'dir'] }) + @IsString() + @Matches(/(^file$)|(^dir$)/) + type: string; + + @ApiProperty({ description: '文件名称' }) + @IsString() + @IsNotEmpty() + @Validate(IsLegalNameExpression) + name: string; +} + +export class GetFileListDto { + @ApiProperty({ description: '分页标识' }) + @IsOptional() + @IsString() + marker: string; + + @ApiProperty({ description: '当前路径' }) + @IsString() + path: string; + + @ApiPropertyOptional({ description: '搜索关键字' }) + @Validate(IsLegalNameExpression) + @ValidateIf(o => !isEmpty(o.key)) + @IsString() + key: string; +} + +export class MKDirDto { + @ApiProperty({ description: '文件夹名称' }) + @IsNotEmpty() + @IsString() + @Validate(IsLegalNameExpression) + dirName: string; + + @ApiProperty({ description: '所属路径' }) + @IsString() + path: string; +} + +export class RenameDto { + @ApiProperty({ description: '文件类型' }) + @IsString() + @Matches(/(^file$)|(^dir$)/) + type: string; + + @ApiProperty({ description: '更改的名称' }) + @IsString() + @IsNotEmpty() + @Validate(IsLegalNameExpression) + toName: string; + + @ApiProperty({ description: '原来的名称' }) + @IsString() + @IsNotEmpty() + @Validate(IsLegalNameExpression) + name: string; + + @ApiProperty({ description: '路径' }) + @IsString() + path: string; +} + +export class FileInfoDto { + @ApiProperty({ description: '文件名' }) + @IsString() + @IsNotEmpty() + @Validate(IsLegalNameExpression) + name: string; + + @ApiProperty({ description: '文件所在路径' }) + @IsString() + path: string; +} + +export class DeleteDto { + @ApiProperty({ description: '需要操作的文件或文件夹', type: [FileOpItem] }) + @Type(() => FileOpItem) + @ArrayMaxSize(NETDISK_HANDLE_MAX_ITEM) + @ValidateNested({ each: true }) + files: FileOpItem[]; + + @ApiProperty({ description: '所在目录' }) + @IsString() + path: string; +} + +export class MarkFileDto { + @ApiProperty({ description: '文件名' }) + @IsString() + @IsNotEmpty() + @Validate(IsLegalNameExpression) + name: string; + + @ApiProperty({ description: '文件所在路径' }) + @IsString() + path: string; + + @ApiProperty({ description: '备注信息' }) + @IsString() + mark: string; +} + +export class FileOpDto { + @ApiProperty({ description: '需要操作的文件或文件夹', type: [FileOpItem] }) + @Type(() => FileOpItem) + @ArrayMaxSize(NETDISK_HANDLE_MAX_ITEM) + @ValidateNested({ each: true }) + files: FileOpItem[]; + + @ApiProperty({ description: '操作前的目录' }) + @IsString() + originPath: string; + + @ApiProperty({ description: '操作后的目录' }) + @IsString() + toPath: string; +} diff --git a/src/modules/netdisk/manager/manage.service.ts b/src/modules/netdisk/manager/manage.service.ts new file mode 100644 index 0000000..9706867 --- /dev/null +++ b/src/modules/netdisk/manager/manage.service.ts @@ -0,0 +1,794 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { isEmpty } from 'lodash'; +import { ConfigKeyPaths } from '~/config'; +import { generateRandomValue } from '~/utils'; +import { SFileInfo, SFileInfoDetail, SFileList } from './manage.class'; +import { InjectMinio } from 'nestjs-minio'; +import { Client } from 'minio'; + +@Injectable() +export class NetDiskManageService { + private get ossConfig() { + return this.configService.get('oss', { infer: true }); + } + constructor( + private configService: ConfigService, + @InjectMinio() private readonly minioClient: Client + ) {} + /** + * 获取文件列表 + * @param prefix 当前文件夹路径,搜索模式下会被忽略 + * @param marker 下一页标识 + * @returns iFileListResult + */ + async getFileList(prefix = '', marker = '', skey = ''): Promise { + // 是否需要搜索 + const searching = !isEmpty(skey); + return new Promise((resolve, reject) => { + try { + const fileStream = this.minioClient.listObjects(this.ossConfig.bucket, prefix, false); + this.readStreamData(fileStream).then(respBody => { + console.log(respBody); + const dirs = respBody.filter(item => 'prefix' in item).map(item => item.prefix); + const files = respBody.filter(item => !('prefix' in item)); + // 如果这个nextMarker不为空,那么还有未列举完毕的文件列表,下次调用listPrefix的时候, + // 指定options里面的marker为这个值 + const fileList: SFileInfo[] = []; + // 处理目录,但只有非搜索模式下可用 + if (!searching && !isEmpty(dirs)) { + // dir + for (const dirPath of dirs) { + const name = (dirPath as string).substr(0, dirPath.length - 1).replace(prefix, ''); + if (isEmpty(skey) || name.includes(skey)) { + fileList.push({ + name: (dirPath as string).substr(0, dirPath.length - 1).replace(prefix, ''), + type: 'dir', + id: generateRandomValue(10) + }); + } + } + } + // handle items + if (!isEmpty(files)) { + // file + for (const item of files) { + // 搜索模式下处理 + // if (searching) { + // const pathList: string[] = item.key.split(NETDISK_DELIMITER); + // // dir is empty stirng, file is key string + // const name = pathList.pop(); + // if ( + // item.key.endsWith(NETDISK_DELIMITER) && + // pathList[pathList.length - 1].includes(skey) + // ) { + // // 结果是目录 + // const ditName = pathList.pop(); + // fileList.push({ + // id: generateRandomValue(10), + // name: ditName, + // type: 'dir', + // belongTo: pathList.join(NETDISK_DELIMITER) + // }); + // } else if (name.includes(skey)) { + // // 文件 + // fileList.push({ + // id: generateRandomValue(10), + // name, + // type: 'file', + // fsize: item.fsize, + // mimeType: item.mimeType, + // putTime: new Date(Number.parseInt(item.putTime) / 10000), + // belongTo: pathList.join(NETDISK_DELIMITER) + // }); + // } + // } else { + // 正常获取列表 + const fileKey = item.name.replace(prefix, '') as string; + if (!isEmpty(fileKey)) { + fileList.push({ + id: generateRandomValue(10), + name: fileKey, + type: 'file', + fsize: `${item.size || 0}`, + mimeType: item.name.split('.').pop(), + putTime: new Date(item.lastModified) + }); + // } + } + } + } + resolve({ + list: fileList + // marker: respBody.marker || null + }); + }); + } catch (e) { + reject(e); + } + }); + } + + /** + * 获取文件信息 + */ + async getFileInfo(name: string, path: string): Promise { + const respBody = await this.minioClient.statObject(this.ossConfig.bucket, `${path}${name}`); + const detailInfo: SFileInfoDetail = { + fsize: respBody.size, + hash: respBody.metaData.hash || '', + md5: respBody.metaData.md5 || '', + mimeType: respBody.metaData['content-type'], + putTime: new Date(respBody.lastModified) + }; + return detailInfo; + } + + // /** + // * 修改文件MimeType + // */ + // async changeFileHeaders( + // name: string, + // path: string, + // headers: { [k: string]: string } + // ): Promise { + // return new Promise((resolve, reject) => { + // this.bucketManager.changeHeaders( + // this.qiniuConfig.bucket, + // `${path}${name}`, + // headers, + // (err, _, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // resolve(); + // } else { + // reject( + // new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + // ); + // } + // } + // ); + // }); + // } + + // /** + // * 创建文件夹 + // * @returns true创建成功 + // */ + // async createDir(dirName: string): Promise { + // const safeDirName = dirName.endsWith('/') ? dirName : `${dirName}/`; + // return new Promise((resolve, reject) => { + // // 上传一个空文件以用于显示文件夹效果 + // const formUploader = new qiniu.form_up.FormUploader(this.config); + // const putExtra = new qiniu.form_up.PutExtra(); + // formUploader.put( + // this.createUploadToken(''), + // safeDirName, + // ' ', + // putExtra, + // (respErr, respBody, respInfo) => { + // if (respErr) { + // reject(respErr); + // return; + // } + // if (respInfo.statusCode === 200) { + // resolve(); + // } else { + // reject( + // new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + // ); + // } + // } + // ); + // }); + // } + + // /** + // * 检查文件是否存在,同可检查目录 + // */ + // async checkFileExist(filePath: string): Promise { + // return new Promise((resolve, reject) => { + // // fix path end must a / + + // // 检测文件夹是否存在 + // this.bucketManager.stat(this.qiniuConfig.bucket, filePath, (respErr, respBody, respInfo) => { + // if (respErr) { + // reject(respErr); + // return; + // } + // if (respInfo.statusCode === 200) { + // // 文件夹存在 + // resolve(true); + // } else if (respInfo.statusCode === 612) { + // // 文件夹不存在 + // resolve(false); + // } else { + // reject( + // new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + // ); + // } + // }); + // }); + // } + + // /** + // * 创建Upload Token, 默认过期时间一小时 + // * @returns upload token + // */ + // createUploadToken(endUser: string): string { + // const policy = new qiniu.rs.PutPolicy({ + // scope: this.qiniuConfig.bucket, + // insertOnly: 1, + // fsizeLimit: 1024 ** 2 * 10, + // endUser + // }); + // const uploadToken = policy.uploadToken(this.mac); + // return uploadToken; + // } + + // /** + // * 重命名文件 + // * @param dir 文件路径 + // * @param name 文件名称 + // */ + // async renameFile(dir: string, name: string, toName: string): Promise { + // const fileName = `${dir}${name}`; + // const toFileName = `${dir}${toName}`; + // const op = { + // force: true + // }; + // return new Promise((resolve, reject) => { + // this.bucketManager.move( + // this.qiniuConfig.bucket, + // fileName, + // this.qiniuConfig.bucket, + // toFileName, + // op, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // } else { + // if (respInfo.statusCode === 200) { + // resolve(); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + // ) + // ); + // } + // } + // } + // ); + // }); + // } + + // /** + // * 移动文件 + // */ + // async moveFile(dir: string, toDir: string, name: string): Promise { + // const fileName = `${dir}${name}`; + // const toFileName = `${toDir}${name}`; + // const op = { + // force: true + // }; + // return new Promise((resolve, reject) => { + // this.bucketManager.move( + // this.qiniuConfig.bucket, + // fileName, + // this.qiniuConfig.bucket, + // toFileName, + // op, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // } else { + // if (respInfo.statusCode === 200) { + // resolve(); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + // ) + // ); + // } + // } + // } + // ); + // }); + // } + + // /** + // * 复制文件 + // */ + // async copyFile(dir: string, toDir: string, name: string): Promise { + // const fileName = `${dir}${name}`; + // // 拼接文件名 + // const ext = extname(name); + // const bn = basename(name, ext); + // const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`; + // const op = { + // force: true + // }; + // return new Promise((resolve, reject) => { + // this.bucketManager.copy( + // this.qiniuConfig.bucket, + // fileName, + // this.qiniuConfig.bucket, + // toFileName, + // op, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // } else { + // if (respInfo.statusCode === 200) { + // resolve(); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + // ) + // ); + // } + // } + // } + // ); + // }); + // } + + // /** + // * 重命名文件夹 + // */ + // async renameDir(path: string, name: string, toName: string): Promise { + // const dirName = `${path}${name}`; + // const toDirName = `${path}${toName}`; + // let hasFile = true; + // let marker = ''; + // const op = { + // force: true + // }; + // const bucketName = this.qiniuConfig.bucket; + // while (hasFile) { + // await new Promise((resolve, reject) => { + // // 列举当前目录下的所有文件 + // this.bucketManager.listPrefix( + // this.qiniuConfig.bucket, + // { + // prefix: dirName, + // limit: NETDISK_HANDLE_MAX_ITEM, + // marker + // }, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // const moveOperations = respBody.items.map(item => { + // const { key } = item; + // const destKey = key.replace(dirName, toDirName); + // return qiniu.rs.moveOp(bucketName, key, bucketName, destKey, op); + // }); + // this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + // if (err2) { + // reject(err2); + // return; + // } + // if (respInfo2.statusCode === 200) { + // if (isEmpty(respBody.marker)) hasFile = false; + // else marker = respBody.marker; + + // resolve(); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + // ) + // ); + // } + // }); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + // ) + // ); + // } + // } + // ); + // }); + // } + // } + + // /** + // * 获取七牛下载的文件url链接 + // * @param key 文件路径 + // * @returns 连接 + // */ + getDownloadLink(key: string): Promise { + return new Promise((resolve, reject) => { + if (this.ossConfig.access === 'public') { + this.minioClient.presignedUrl( + 'GET', + this.ossConfig.bucket, + key, + 24 * 60 * 60, + (err, presignedUrl) => { + if (err) reject(); + resolve(presignedUrl); + } + ); + } else if (this.ossConfig.access === 'private') { + // return this.bucketManager.privateDownloadUrl( + // this.qiniuConfig.domain, + // key, + // Date.now() / 1000 + 36000 + // ); + } + }); + } + + // /** + // * 删除文件 + // * @param dir 删除的文件夹目录 + // * @param name 文件名 + // */ + // async deleteFile(dir: string, name: string): Promise { + // return new Promise((resolve, reject) => { + // this.bucketManager.delete( + // this.qiniuConfig.bucket, + // `${dir}${name}`, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // resolve(); + // } else { + // reject( + // new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + // ); + // } + // } + // ); + // }); + // } + + // /** + // * 删除文件夹 + // * @param dir 文件夹所在的上级目录 + // * @param name 文件目录名称 + // */ + // async deleteMultiFileOrDir(fileList: FileOpItem[], dir: string): Promise { + // const files = fileList.filter(item => item.type === 'file'); + // if (files.length > 0) { + // // 批处理文件 + // const copyOperations = files.map(item => { + // const fileName = `${dir}${item.name}`; + // return qiniu.rs.deleteOp(this.qiniuConfig.bucket, fileName); + // }); + // await new Promise((resolve, reject) => { + // this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // resolve(); + // } else if (respInfo.statusCode === 298) { + // reject(new Error('操作异常,但部分文件夹删除成功')); + // } else { + // reject( + // new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + // ); + // } + // }); + // }); + // } + // // 处理文件夹 + // const dirs = fileList.filter(item => item.type === 'dir'); + // if (dirs.length > 0) { + // // 处理文件夹的复制 + // for (let i = 0; i < dirs.length; i++) { + // const dirName = `${dir}${dirs[i].name}/`; + // let hasFile = true; + // let marker = ''; + // while (hasFile) { + // await new Promise((resolve, reject) => { + // // 列举当前目录下的所有文件 + // this.bucketManager.listPrefix( + // this.qiniuConfig.bucket, + // { + // prefix: dirName, + // limit: NETDISK_HANDLE_MAX_ITEM, + // marker + // }, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // const moveOperations = respBody.items.map(item => { + // const { key } = item; + // return qiniu.rs.deleteOp(this.qiniuConfig.bucket, key); + // }); + // this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + // if (err2) { + // reject(err2); + // return; + // } + // if (respInfo2.statusCode === 200) { + // if (isEmpty(respBody.marker)) hasFile = false; + // else marker = respBody.marker; + + // resolve(); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + // ) + // ); + // } + // }); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + // ) + // ); + // } + // } + // ); + // }); + // } + // } + // } + // } + + // /** + // * 复制文件,含文件夹 + // */ + // async copyMultiFileOrDir(fileList: FileOpItem[], dir: string, toDir: string): Promise { + // const files = fileList.filter(item => item.type === 'file'); + // const op = { + // force: true + // }; + // if (files.length > 0) { + // // 批处理文件 + // const copyOperations = files.map(item => { + // const fileName = `${dir}${item.name}`; + // // 拼接文件名 + // const ext = extname(item.name); + // const bn = basename(item.name, ext); + // const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`; + // return qiniu.rs.copyOp( + // this.qiniuConfig.bucket, + // fileName, + // this.qiniuConfig.bucket, + // toFileName, + // op + // ); + // }); + // await new Promise((resolve, reject) => { + // this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // resolve(); + // } else if (respInfo.statusCode === 298) { + // reject(new Error('操作异常,但部分文件夹删除成功')); + // } else { + // reject( + // new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + // ); + // } + // }); + // }); + // } + // // 处理文件夹 + // const dirs = fileList.filter(item => item.type === 'dir'); + // if (dirs.length > 0) { + // // 处理文件夹的复制 + // for (let i = 0; i < dirs.length; i++) { + // const dirName = `${dir}${dirs[i].name}/`; + // const copyDirName = `${toDir}${dirs[i].name}${NETDISK_COPY_SUFFIX}/`; + // let hasFile = true; + // let marker = ''; + // while (hasFile) { + // await new Promise((resolve, reject) => { + // // 列举当前目录下的所有文件 + // this.bucketManager.listPrefix( + // this.qiniuConfig.bucket, + // { + // prefix: dirName, + // limit: NETDISK_HANDLE_MAX_ITEM, + // marker + // }, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // const moveOperations = respBody.items.map(item => { + // const { key } = item; + // const destKey = key.replace(dirName, copyDirName); + // return qiniu.rs.copyOp( + // this.qiniuConfig.bucket, + // key, + // this.qiniuConfig.bucket, + // destKey, + // op + // ); + // }); + // this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + // if (err2) { + // reject(err2); + // return; + // } + // if (respInfo2.statusCode === 200) { + // if (isEmpty(respBody.marker)) hasFile = false; + // else marker = respBody.marker; + + // resolve(); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + // ) + // ); + // } + // }); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + // ) + // ); + // } + // } + // ); + // }); + // } + // } + // } + // } + + // /** + // * 移动文件,含文件夹 + // */ + // async moveMultiFileOrDir(fileList: FileOpItem[], dir: string, toDir: string): Promise { + // const files = fileList.filter(item => item.type === 'file'); + // const op = { + // force: true + // }; + // if (files.length > 0) { + // // 批处理文件 + // const copyOperations = files.map(item => { + // const fileName = `${dir}${item.name}`; + // const toFileName = `${toDir}${item.name}`; + // return qiniu.rs.moveOp( + // this.qiniuConfig.bucket, + // fileName, + // this.qiniuConfig.bucket, + // toFileName, + // op + // ); + // }); + // await new Promise((resolve, reject) => { + // this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // resolve(); + // } else if (respInfo.statusCode === 298) { + // reject(new Error('操作异常,但部分文件夹删除成功')); + // } else { + // reject( + // new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`) + // ); + // } + // }); + // }); + // } + // // 处理文件夹 + // const dirs = fileList.filter(item => item.type === 'dir'); + // if (dirs.length > 0) { + // // 处理文件夹的复制 + // for (let i = 0; i < dirs.length; i++) { + // const dirName = `${dir}${dirs[i].name}/`; + // const toDirName = `${toDir}${dirs[i].name}/`; + // // 移动的目录不是是自己 + // if (toDirName.startsWith(dirName)) continue; + + // let hasFile = true; + // let marker = ''; + // while (hasFile) { + // await new Promise((resolve, reject) => { + // // 列举当前目录下的所有文件 + // this.bucketManager.listPrefix( + // this.qiniuConfig.bucket, + // { + // prefix: dirName, + // limit: NETDISK_HANDLE_MAX_ITEM, + // marker + // }, + // (err, respBody, respInfo) => { + // if (err) { + // reject(err); + // return; + // } + // if (respInfo.statusCode === 200) { + // const moveOperations = respBody.items.map(item => { + // const { key } = item; + // const destKey = key.replace(dirName, toDirName); + // return qiniu.rs.moveOp( + // this.qiniuConfig.bucket, + // key, + // this.qiniuConfig.bucket, + // destKey, + // op + // ); + // }); + // this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => { + // if (err2) { + // reject(err2); + // return; + // } + // if (respInfo2.statusCode === 200) { + // if (isEmpty(respBody.marker)) hasFile = false; + // else marker = respBody.marker; + + // resolve(); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}` + // ) + // ); + // } + // }); + // } else { + // reject( + // new Error( + // `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}` + // ) + // ); + // } + // } + // ); + // }); + // } + // } + // } + // } + /** + * + * @param stream 文件流 + * @returns [] + */ + readStreamData(stream): Promise { + return new Promise((resolve, reject) => { + const result: T[] = []; + stream + .on('data', function (row) { + result.push(row); + }) + .on('end', function () { + resolve(result); + }) + .on('error', function (error) { + reject(error); + }); + }); + } +} diff --git a/src/modules/netdisk/minio/minio.service.ts b/src/modules/netdisk/minio/minio.service.ts new file mode 100644 index 0000000..294f16f --- /dev/null +++ b/src/modules/netdisk/minio/minio.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as Minio from 'minio'; +import { ConfigKeyPaths } from '~/config'; +@Injectable() +export class MinioService { + +} diff --git a/src/modules/netdisk/netdisk.module.ts b/src/modules/netdisk/netdisk.module.ts new file mode 100644 index 0000000..cc067e2 --- /dev/null +++ b/src/modules/netdisk/netdisk.module.ts @@ -0,0 +1,89 @@ +import { Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { RouterModule } from '@nestjs/core'; + +import { UserModule } from '../user/user.module'; + +import { NetDiskManageController } from './manager/manage.controller'; +import { NetDiskOverviewController } from './overview/overview.controller'; +import { NetDiskOverviewService } from './overview/overview.service'; +import { MinioService } from './minio/minio.service'; +import { NetDiskManageService } from './manager/manage.service'; +import { NestMinioModule } from 'nestjs-minio'; +// const getMinioConfig = () => { +// const configService = new ConfigService(); +// const endPoint = configService.get('MINIO_ENDPOINT', 'localhost'); +// const accessKey = configService.get('MINIO_ACCESSKEY', 'accessKey'); +// const secretKey = configService.get('MINIO_SECRET_KEY', 'secretKey'); + +// return NestMinioModule.register({ +// isGlobal: true, +// endPoint, +// port: 9000, +// accessKey, +// secretKey, +// useSSL: false +// }); +// }; + +@Module({ + imports: [ + UserModule, + RouterModule.register([ + { + path: 'netdisk', + module: NetdiskModule + } + ]), + // getMinioConfig() + NestMinioModule.registerAsync({ + inject: [ConfigService], + isGlobal: true, + useFactory: async (configService: ConfigService) => { + const ossConfig = configService.get('oss'); + return { + endPoint: ossConfig.domain, + port: ossConfig.port, + useSSL: ossConfig.useSSL, + accessKey: ossConfig.accessKey, + secretKey: ossConfig.secretKey + }; + } + // endPoint: 'play.min.io', + // port: 9000, + // useSSL: true, + // accessKey: 'Q3AM3UQ867SPQQA43P2F', + // secretKey: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG' + }) + ], + controllers: [NetDiskManageController, NetDiskOverviewController], + providers: [NetDiskManageService, NetDiskOverviewService, MinioService] +}) +export class NetdiskModule {} +// TypeOrmModule.forRootAsync({ +// inject: [ConfigService], +// useFactory: (configService: ConfigService) => { +// let loggerOptions: LoggerOptions = env('DB_LOGGING') as 'all'; + +// try { +// // 解析成 js 数组 ['error'] +// loggerOptions = JSON.parse(loggerOptions); +// } catch { +// // ignore +// } + +// return { +// ...configService.get('database'), +// autoLoadEntities: true, +// logging: loggerOptions, +// logger: new TypeORMLogger(loggerOptions) +// }; +// }, +// // dataSource receives the configured DataSourceOptions +// // and returns a Promise. +// dataSourceFactory: async options => { +// const dataSource = await new DataSource(options).initialize(); +// return dataSource; +// } +// }) +// ], diff --git a/src/modules/netdisk/overview/overview.controller.ts b/src/modules/netdisk/overview/overview.controller.ts new file mode 100644 index 0000000..20a8ae7 --- /dev/null +++ b/src/modules/netdisk/overview/overview.controller.ts @@ -0,0 +1,41 @@ +import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager'; +import { Controller, Get, UseInterceptors } from '@nestjs/common'; +import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; + +import { OverviewSpaceInfo } from './overview.dto'; +import { NetDiskOverviewService } from './overview.service'; + +export const permissions = definePermission('netdisk:overview', { + DESC: 'desc' +} as const); + +@ApiTags('NetDiskOverview - 网盘概览模块') +@Controller('overview') +export class NetDiskOverviewController { + constructor(private overviewService: NetDiskOverviewService) {} + + @Get('desc') + @CacheKey('netdisk_overview_desc') + @CacheTTL(3600) + @UseInterceptors(CacheInterceptor) + @ApiOperation({ summary: '获取网盘空间数据统计' }) + @ApiOkResponse({ type: OverviewSpaceInfo }) + @Perm(permissions.DESC) + async space(): Promise { + const date = this.overviewService.getZeroHourAnd1Day(new Date()); + const hit = await this.overviewService.getHit(date); + const flow = await this.overviewService.getFlow(date); + const space = await this.overviewService.getSpace(date); + const count = await this.overviewService.getCount(date); + return { + fileSize: count.datas[count.datas.length - 1], + flowSize: flow.datas[flow.datas.length - 1], + hitSize: hit.datas[hit.datas.length - 1], + spaceSize: space.datas[space.datas.length - 1], + flowTrend: flow, + sizeTrend: space + }; + } +} diff --git a/src/modules/netdisk/overview/overview.dto.ts b/src/modules/netdisk/overview/overview.dto.ts new file mode 100644 index 0000000..9d77aff --- /dev/null +++ b/src/modules/netdisk/overview/overview.dto.ts @@ -0,0 +1,53 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class SpaceInfo { + @ApiProperty({ description: '当月的X号', type: [Number] }) + times: number[]; + + @ApiProperty({ description: '对应天数的容量, byte单位', type: [Number] }) + datas: number[]; +} + +export class CountInfo { + @ApiProperty({ description: '当月的X号', type: [Number] }) + times: number[]; + + @ApiProperty({ description: '对应天数的文件数量', type: [Number] }) + datas: number[]; +} + +export class FlowInfo { + @ApiProperty({ description: '当月的X号', type: [Number] }) + times: number[]; + + @ApiProperty({ description: '对应天数的耗费流量', type: [Number] }) + datas: number[]; +} + +export class HitInfo { + @ApiProperty({ description: '当月的X号', type: [Number] }) + times: number[]; + + @ApiProperty({ description: '对应天数的Get请求次数', type: [Number] }) + datas: number[]; +} + +export class OverviewSpaceInfo { + @ApiProperty({ description: '当前使用容量' }) + spaceSize: number; + + @ApiProperty({ description: '当前文件数量' }) + fileSize: number; + + @ApiProperty({ description: '当天使用流量' }) + flowSize: number; + + @ApiProperty({ description: '当天请求次数' }) + hitSize: number; + + @ApiProperty({ description: '流量趋势,从当月1号开始计算', type: FlowInfo }) + flowTrend: FlowInfo; + + @ApiProperty({ description: '容量趋势,从当月1号开始计算', type: SpaceInfo }) + sizeTrend: SpaceInfo; +} diff --git a/src/modules/netdisk/overview/overview.service.ts b/src/modules/netdisk/overview/overview.service.ts new file mode 100644 index 0000000..1f8a352 --- /dev/null +++ b/src/modules/netdisk/overview/overview.service.ts @@ -0,0 +1,165 @@ +import { HttpService } from '@nestjs/axios'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import dayjs from 'dayjs'; +import * as qiniu from 'qiniu'; + +import { ConfigKeyPaths } from '~/config'; +import { OSS_API } from '~/constants/oss.constant'; + +import { CountInfo, FlowInfo, HitInfo, SpaceInfo } from './overview.dto'; + +@Injectable() +export class NetDiskOverviewService { + private mac: qiniu.auth.digest.Mac; + private readonly FORMAT = 'YYYYMMDDHHmmss'; + private get qiniuConfig() { + return this.configService.get('oss', { infer: true }); + } + + constructor( + private configService: ConfigService, + private readonly httpService: HttpService + ) { + this.mac = new qiniu.auth.digest.Mac(this.qiniuConfig.accessKey, this.qiniuConfig.secretKey); + } + + /** 获取格式化后的起始和结束时间 */ + getStartAndEndDate(start: Date, end = new Date()) { + return [dayjs(start).format(this.FORMAT), dayjs(end).format(this.FORMAT)]; + } + + /** + * 获取数据统计接口路径 + * @see: https://developer.qiniu.com/kodo/3906/statistic-interface + */ + getStatisticUrl(type: string, queryParams = {}) { + const defaultParams = { + $bucket: this.qiniuConfig.bucket, + g: 'day' + }; + const searchParams = new URLSearchParams({ ...defaultParams, ...queryParams }); + return decodeURIComponent(`${OSS_API}/v6/${type}?${searchParams}`); + } + + /** 获取统计数据 */ + getStatisticData(url: string) { + const accessToken = qiniu.util.generateAccessTokenV2( + this.mac, + url, + 'GET', + 'application/x-www-form-urlencoded' + ); + return this.httpService.axiosRef.get(url, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `${accessToken}` + } + }); + } + + /** + * 获取当天零时 + */ + getZeroHourToDay(current: Date): Date { + const year = dayjs(current).year(); + const month = dayjs(current).month(); + const date = dayjs(current).date(); + return new Date(year, month, date, 0); + } + + /** + * 获取当月1号零时 + */ + getZeroHourAnd1Day(current: Date): Date { + const year = dayjs(current).year(); + const month = dayjs(current).month(); + return new Date(year, month, 1, 0); + } + + /** + * 该接口可以获取标准存储的当前存储量。可查询当天计量,统计延迟大概 5 分钟。 + * https://developer.qiniu.com/kodo/3908/statistic-space + */ + async getSpace(beginDate: Date, endDate = new Date()): Promise { + const [begin, end] = this.getStartAndEndDate(beginDate, endDate); + const url = this.getStatisticUrl('space', { begin, end }); + const { data } = await this.getStatisticData(url); + return { + datas: data.datas, + times: data.times.map(e => { + return dayjs.unix(e).date(); + }) + }; + } + + /** + * 该接口可以获取标准存储的文件数量。可查询当天计量,统计延迟大概 5 分钟。 + * https://developer.qiniu.com/kodo/3914/count + */ + async getCount(beginDate: Date, endDate = new Date()): Promise { + const [begin, end] = this.getStartAndEndDate(beginDate, endDate); + const url = this.getStatisticUrl('count', { begin, end }); + const { data } = await this.getStatisticData(url); + return { + times: data.times.map(e => { + return dayjs.unix(e).date(); + }), + datas: data.datas + }; + } + + /** + * 外网流出流量统计 + * 该接口可以获取外网流出流量、CDN回源流量统计和 GET 请求次数。可查询当天计量,统计延迟大概 5 分钟。 + * https://developer.qiniu.com/kodo/3820/blob-io + */ + async getFlow(beginDate: Date, endDate = new Date()): Promise { + const [begin, end] = this.getStartAndEndDate(beginDate, endDate); + const url = this.getStatisticUrl('blob_io', { + begin, + end, + $ftype: 0, + $src: 'origin', + select: 'flow' + }); + const { data } = await this.getStatisticData(url); + const times = []; + const datas = []; + data.forEach(e => { + times.push(dayjs(e.time).date()); + datas.push(e.values.flow); + }); + return { + times, + datas + }; + } + + /** + * GET 请求次数统计 + * 该接口可以获取外网流出流量、CDN回源流量统计和 GET 请求次数。可查询当天计量,统计延迟大概 5 分钟。 + * https://developer.qiniu.com/kodo/3820/blob-io + */ + async getHit(beginDate: Date, endDate = new Date()): Promise { + const [begin, end] = this.getStartAndEndDate(beginDate, endDate); + const url = this.getStatisticUrl('blob_io', { + begin, + end, + $ftype: 0, + $src: 'inner', + select: 'hit' + }); + const { data } = await this.getStatisticData(url); + const times = []; + const datas = []; + data.forEach(e => { + times.push(dayjs(e.time).date()); + datas.push(e.values.hit); + }); + return { + times, + datas + }; + } +} diff --git a/src/modules/product/product.controller.ts b/src/modules/product/product.controller.ts new file mode 100644 index 0000000..d91bb61 --- /dev/null +++ b/src/modules/product/product.controller.ts @@ -0,0 +1,80 @@ +import { + Body, + Controller, + Get, + Query, + Put, + Delete, + Post, + BadRequestException +} from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Perm, definePermission } from '../auth/decorators/permission.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { ProductService } from './product.service'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { ProductEntity } from './product.entity'; +import { ProductDto, ProductQueryDto, ProductUpdateDto } from './product.dto'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { Domain, SkDomain } from '~/common/decorators/domain.decorator'; +export const permissions = definePermission('app:product', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('Product - 产品') +@ApiSecurityAuth() +@Controller('product') +export class ProductController { + constructor(private productService: ProductService) {} + + @Get() + @ApiOperation({ summary: '获取产品列表' }) + @ApiResult({ type: [ProductEntity], isPage: true }) + @Perm(permissions.LIST) + async list(@Domain() domain: SkDomain, @Query() dto: ProductQueryDto) { + return this.productService.findAll({ ...dto, domain }); + } + + @Get(':id') + @ApiOperation({ summary: '获取产品信息' }) + @ApiResult({ type: ProductDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.productService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增产品' }) + @Perm(permissions.CREATE) + async create(@Domain() domain: SkDomain, @Body() dto: ProductDto): Promise { + await this.productService.create({ ...dto, domain }); + } + + @Put(':id') + @ApiOperation({ summary: '更新产品' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: ProductUpdateDto): Promise { + await this.productService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除产品' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.productService.delete(id); + } + + @Put('unlink-attachments/:id') + @ApiOperation({ summary: '附件解除关联' }) + @Perm(permissions.UPDATE) + async unlinkAttachments( + @IdParam() id: number, + @Body() { fileIds }: ProductUpdateDto + ): Promise { + await this.productService.unlinkAttachments(id, fileIds); + } +} diff --git a/src/modules/product/product.dto.ts b/src/modules/product/product.dto.ts new file mode 100644 index 0000000..f476345 --- /dev/null +++ b/src/modules/product/product.dto.ts @@ -0,0 +1,62 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { Storage } from '../tools/storage/storage.entity'; +import { DomainType } from '~/common/decorators/domain.decorator'; + +export class ProductDto extends DomainType { + @ApiProperty({ description: '产品名称' }) + @IsString() + name: string; + + @ApiProperty({ description: '产品规格' }) + @IsOptional() + @IsString() + productSpecification: string; + + @ApiProperty({ description: '产品备注' }) + @IsOptional() + @IsString() + remark: string; + + @ApiProperty({ description: '单位(字典)' }) + @IsOptional() + @IsNumber() + unitId: number; + + @ApiProperty({ description: '所属公司' }) + @IsOptional() + @IsNumber() + companyId: number; + + @ApiProperty({ description: '附件' }) + files: Storage[]; +} + +export class ProductUpdateDto extends PartialType(ProductDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} + +export class ProductQueryDto extends IntersectionType( + PagerDto, + PartialType(ProductDto), + DomainType +) { + @ApiProperty({ description: '所属公司名称' }) + @IsOptional() + @IsString() + company: string; + + @ApiProperty({ description: '产品名字' }) + @IsOptional() + @IsString() + name?: string; + + @ApiProperty({ description: '关键字(名字/编号/规格)' }) + @IsOptional() + @IsString() + keyword?: string; +} diff --git a/src/modules/product/product.entity.ts b/src/modules/product/product.entity.ts new file mode 100644 index 0000000..a424b87 --- /dev/null +++ b/src/modules/product/product.entity.ts @@ -0,0 +1,109 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { + BeforeInsert, + BeforeUpdate, + Column, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + Relation +} from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; +import { Storage } from '../tools/storage/storage.entity'; +import { CompanyEntity } from '../company/company.entity'; +import pinyin from 'pinyin'; +import { DictItemEntity } from '../system/dict-item/dict-item.entity'; +import { SkDomain } from '~/common/decorators/domain.decorator'; +@Entity({ name: 'product' }) +export class ProductEntity extends CommonEntity { + @Column({ + name: 'product_number', + type: 'varchar', + length: 255, + comment: '产品编号' + }) + @ApiProperty({ description: '产品编号' }) + productNumber: string; + + @Column({ + name: 'name', + type: 'varchar', + length: 255, + comment: '产品名称' + }) + @ApiProperty({ description: '产品名称' }) + name: string; + + @Column({ + name: 'product_specification', + type: 'varchar', + nullable: true, + length: 255, + comment: '产品规格' + }) + @ApiProperty({ description: '产品规格', nullable: true }) + productSpecification?: string; + + @Column({ + name: 'remark', + type: 'varchar', + length: 255, + nullable: true, + comment: '备注' + }) + @ApiProperty({ description: '产品备注' }) + remark: string; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; + + @Column({ name: 'company_id', type: 'int', comment: '所属公司', nullable: true }) + @ApiProperty({ description: '所属公司' }) + companyId: number; + + @Column({ name: 'unit_id', type: 'int', comment: '单位(字典)', nullable: true }) + @ApiProperty({ description: '单位(字典)' }) + unitId: number; + + @ManyToOne(() => DictItemEntity) + @JoinColumn({ name: 'unit_id' }) + unit: DictItemEntity; + + @Column({ type: 'int', default: 1, comment: '所属域' }) + @ApiProperty({ description: '所属域' }) + domain: SkDomain; + + @ApiHideProperty() + @Column({ + name: 'name_pinyin', + type: 'varchar', + length: 255, + nullable: true, + comment: '产品名称的拼音' + }) + namePinyin: string; + + @BeforeInsert() + @BeforeUpdate() + updateNamePinyin() { + this.namePinyin = pinyin(this.name, { + style: pinyin.STYLE_NORMAL, + heteronym: false + }).join(''); + } + + @ManyToOne(() => CompanyEntity /* , { onDelete: 'CASCADE' } */) + @JoinColumn({ name: 'company_id' }) + company: CompanyEntity; + + @ManyToMany(() => Storage, storage => storage.products) + @JoinTable({ + name: 'product_storage', + joinColumn: { name: 'product_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } + }) + files: Relation; +} diff --git a/src/modules/product/product.module.ts b/src/modules/product/product.module.ts new file mode 100644 index 0000000..14e2367 --- /dev/null +++ b/src/modules/product/product.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ProductController } from './product.controller'; +import { ProductService } from './product.service'; +import { ProductEntity } from './product.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StorageModule } from '../tools/storage/storage.module'; +import { ParamConfigModule } from '../system/param-config/param-config.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([ProductEntity]), StorageModule, ParamConfigModule], + controllers: [ProductController], + providers: [ProductService] +}) +export class ProductModule {} diff --git a/src/modules/product/product.service.ts b/src/modules/product/product.service.ts new file mode 100644 index 0000000..fc6c9fc --- /dev/null +++ b/src/modules/product/product.service.ts @@ -0,0 +1,194 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { ProductEntity } from './product.entity'; +import { EntityManager, Like, Repository } from 'typeorm'; +import { ProductDto, ProductQueryDto, ProductUpdateDto } from './product.dto'; +import { Pagination } from '~/helper/paginate/pagination'; +import { paginate } from '~/helper/paginate'; +import { Storage } from '../tools/storage/storage.entity'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { fieldSearch } from '~/shared/database/field-search'; +import { ParamConfigEnum } from '~/constants/enum'; +import { ParamConfigEntity } from '../system/param-config/param-config.entity'; + +@Injectable() +export class ProductService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(ProductEntity) + private productRepository: Repository, + @InjectRepository(Storage) + private storageRepository: Repository, + @InjectRepository(ParamConfigEntity) + private paramConfigRepository: Repository + ) {} + + /** + * 查询所有产品 + */ + async findAll({ + page, + pageSize, + ...fields + }: ProductQueryDto): Promise> { + const { company: companyName, keyword, ...ext } = fields; + const sqb = this.productRepository + .createQueryBuilder('product') + .leftJoin('product.files', 'files') + .leftJoin('product.company', 'company') + .leftJoin('product.unit', 'unit') + .addSelect(['files.id', 'files.path', 'company.name', 'company.id', 'unit.id', 'unit.label']) + .where(fieldSearch(ext)) + .andWhere('product.isDelete = 0') + .addOrderBy('product.namePinyin', 'ASC'); + if (companyName) { + sqb.andWhere({ + company: { + name: Like(`%${companyName}%`) + } + }); + } + if (keyword) { + //关键字模糊查询product的name,productNumber,productSpecification + sqb.andWhere( + '(product.name like :keyword or product.productNumber like :keyword or product.productSpecification like :keyword)', + { + keyword: `%${keyword}%` + } + ); + } + return paginate(sqb, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create(dto: ProductDto): Promise { + const { name, companyId, productSpecification } = dto; + const isExsit = await this.productRepository.findOne({ + where: { productSpecification, name, company: { id: companyId } } + }); + if (isExsit) { + throw new BusinessException(ErrorEnum.PRODUCT_EXIST); + } + await this.productRepository.insert( + this.productRepository.create({ ...dto, productNumber: await this.generateProductNumber() }) + ); + } + + /** + * 更新 + */ + async update(id: number, { fileIds, ...data }: Partial): Promise { + await this.entityManager.transaction(async manager => { + const { name, companyId, productSpecification } = data; + const isExsit = await this.productRepository.findOne({ + where: { productSpecification, name, company: { id: companyId } } + }); + if (isExsit) { + throw new BusinessException(ErrorEnum.PRODUCT_EXIST); + } + await manager.update( + ProductEntity, + id, + this.productRepository.create({ + ...data + }) + ); + const product = await this.productRepository + .createQueryBuilder('product') + .leftJoinAndSelect('product.files', 'files') + .where('product.id = :id', { id }) + .getOne(); + if (fileIds?.length) { + const count = await this.storageRepository + .createQueryBuilder('storage') + .where('storage.id in(:fileIds)', { fileIds }) + .getCount(); + if (count !== fileIds?.length) { + throw new BusinessException(ErrorEnum.STORAGE_NOT_FOUND); + } + // 附件要批量插入 + await manager.createQueryBuilder().relation(ProductEntity, 'files').of(id).add(fileIds); + } + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + await this.productRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个产品信息 + */ + async info(id: number) { + const info = await this.productRepository + .createQueryBuilder('product') + .leftJoin('product.company', 'company') + .addSelect(['company.name', 'company.id']) + .where({ + id + }) + .andWhere('product.isDelete = 0') + .getOne(); + return info; + } + + /** + * 解除附件关联 + * @param id 产品ID + * @param fileIds 附件ID + */ + async unlinkAttachments(id: number, fileIds: number[]) { + await this.entityManager.transaction(async manager => { + const product = await this.productRepository + .createQueryBuilder('product') + .leftJoinAndSelect('product.files', 'files') + .where('product.id = :id', { id }) + .getOne(); + const linkedFiles = product.files + .map(item => item.id) + .filter(item => !fileIds.includes(item)); + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(ProductEntity, 'files') + .of(id) + .addAndRemove(linkedFiles, product.files); + }); + } + + /** + * 生成产品编号 + * @returns 产品编号 + */ + async generateProductNumber(): Promise { + const prefix = + ( + await this.paramConfigRepository.findOne({ + where: { + key: ParamConfigEnum.ProductNumberPrefix + } + }) + )?.value || ''; + const lastProduct = await this.productRepository + .createQueryBuilder('product') + .select( + `MAX(CAST(REPLACE(COALESCE(product.product_number, ''), '${prefix}', '') AS UNSIGNED))`, + 'productNumber' + ) + .getRawOne(); + const lastNumber = lastProduct.productNumber + ? parseInt(lastProduct.productNumber.replace(prefix, '')) + : 0; + const newNumber = lastNumber + 1 < 1000 ? 1000 : lastNumber + 1; + return `${prefix}${newNumber}`; + } +} diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts new file mode 100644 index 0000000..bd92a28 --- /dev/null +++ b/src/modules/project/project.controller.ts @@ -0,0 +1,80 @@ +import { + Body, + Controller, + Get, + Query, + Put, + Delete, + Post, + BadRequestException +} from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Perm, definePermission } from '../auth/decorators/permission.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { ProjectService } from './project.service'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { ProjectEntity } from './project.entity'; +import { ProjectDto, ProjectQueryDto, ProjectUpdateDto } from './project.dto'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { Domain, SkDomain } from '~/common/decorators/domain.decorator'; +export const permissions = definePermission('app:project', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('Project - 项目') +@ApiSecurityAuth() +@Controller('project') +export class ProjectController { + constructor(private projectService: ProjectService) {} + + @Get() + @ApiOperation({ summary: '分页获取项目列表' }) + @ApiResult({ type: [ProjectEntity], isPage: true }) + @Perm(permissions.LIST) + async list(@Domain() domain: SkDomain, @Query() dto: ProjectQueryDto) { + return this.projectService.findAll({ ...dto, domain }); + } + + @Get(':id') + @ApiOperation({ summary: '获取项目信息' }) + @ApiResult({ type: ProjectDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.projectService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增项目' }) + @Perm(permissions.CREATE) + async create(@Domain() domain: SkDomain, @Body() dto: ProjectDto): Promise { + await this.projectService.create({ ...dto, domain }); + } + + @Put(':id') + @ApiOperation({ summary: '更新项目' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: ProjectUpdateDto): Promise { + await this.projectService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除项目' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.projectService.delete(id); + } + + @Put('unlink-attachments/:id') + @ApiOperation({ summary: '附件解除关联' }) + @Perm(permissions.UPDATE) + async unlinkAttachments( + @IdParam() id: number, + @Body() { fileIds }: ProjectUpdateDto + ): Promise { + await this.projectService.unlinkAttachments(id, fileIds); + } +} diff --git a/src/modules/project/project.dto.ts b/src/modules/project/project.dto.ts new file mode 100644 index 0000000..3e02fc5 --- /dev/null +++ b/src/modules/project/project.dto.ts @@ -0,0 +1,42 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { IsArray, IsOptional, IsString } from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { Storage } from '../tools/storage/storage.entity'; +import { IsUnique } from '~/shared/database/constraints/unique.constraint'; +import { ProjectEntity } from './project.entity'; +import { DomainType } from '~/common/decorators/domain.decorator'; + +export class ProjectDto extends DomainType { + @ApiProperty({ description: '项目名称' }) + @IsUnique(ProjectEntity, { message: '已存在同名项目' }) + @IsString() + name: string; + + @ApiProperty({ description: '附件' }) + files: Storage[]; +} + +export class ProjectUpdateDto extends PartialType(ProjectDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} + +export class ComapnyCreateDto extends PartialType(ProjectDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} + +export class ProjectQueryDto extends IntersectionType( + PagerDto, + PartialType(ProjectDto), + DomainType +) { + @ApiProperty({ description: '项目名称' }) + @IsOptional() + @IsString() + name: string; +} diff --git a/src/modules/project/project.entity.ts b/src/modules/project/project.entity.ts new file mode 100644 index 0000000..72a7ec9 --- /dev/null +++ b/src/modules/project/project.entity.ts @@ -0,0 +1,43 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinTable, ManyToMany, OneToMany, Relation } from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; +import { Storage } from '../tools/storage/storage.entity'; +import { ProductEntity } from '../product/product.entity'; +import { MaterialsInOutEntity } from '../materials_inventory/in_out/materials_in_out.entity'; +import { SkDomain } from '~/common/decorators/domain.decorator'; + +/** + * 项目实体类 + */ +@Entity({ name: 'project' }) +export class ProjectEntity extends CommonEntity { + @Column({ + name: 'name', + type: 'varchar', + unique: true, + length: 255, + comment: '项目名称' + }) + @ApiProperty({ description: '项目名称' }) + name: string; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; + + @Column({ type: 'int', default: 1, comment: '所属域' }) + @ApiProperty({ description: '所属域' }) + domain: SkDomain; + + @ApiHideProperty() + @OneToMany(() => MaterialsInOutEntity, product => product.project) + materialsInOuts: Relation; + + @ManyToMany(() => Storage, storage => storage.projects) + @JoinTable({ + name: 'project_storage', + joinColumn: { name: 'project_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } + }) + files: Relation; +} diff --git a/src/modules/project/project.module.ts b/src/modules/project/project.module.ts new file mode 100644 index 0000000..170c6eb --- /dev/null +++ b/src/modules/project/project.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ProjectController } from './project.controller'; +import { ProjectService } from './project.service'; +import { ProjectEntity } from './project.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StorageModule } from '../tools/storage/storage.module'; +import { DatabaseModule } from '~/shared/database/database.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([ProjectEntity]), StorageModule, DatabaseModule], + controllers: [ProjectController], + providers: [ProjectService] +}) +export class ProjectModule {} diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts new file mode 100644 index 0000000..4b56820 --- /dev/null +++ b/src/modules/project/project.service.ts @@ -0,0 +1,122 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { ProjectEntity } from './project.entity'; +import { EntityManager, Like, Repository } from 'typeorm'; +import { ProjectDto, ProjectQueryDto, ProjectUpdateDto } from './project.dto'; +import { Pagination } from '~/helper/paginate/pagination'; +import { paginate } from '~/helper/paginate'; +import { Storage } from '../tools/storage/storage.entity'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { fieldSearch } from '~/shared/database/field-search'; + +@Injectable() +export class ProjectService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(ProjectEntity) + private projectRepository: Repository, + @InjectRepository(Storage) + private storageRepository: Repository + ) {} + + /** + * 分页查询所有 + */ + async findAll({ + page, + pageSize, + ...fields + }: ProjectQueryDto): Promise> { + const queryBuilder = this.projectRepository + .createQueryBuilder('project') + .leftJoin('project.files', 'files') + .addSelect(['files.id', 'files.path']) + .where(fieldSearch(fields)) + .andWhere('project.isDelete = 0'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create(dto: ProjectDto): Promise { + await this.projectRepository.insert(dto); + } + + /** + * 更新 + */ + async update(id: number, { fileIds, ...data }: Partial): Promise { + await this.entityManager.transaction(async manager => { + await manager.update(ProjectEntity, id, { + ...data + }); + if (fileIds?.length) { + const count = await this.storageRepository + .createQueryBuilder('storage') + .where('storage.id in(:fileIds)', { fileIds }) + .getCount(); + if (count !== fileIds?.length) { + throw new BusinessException(ErrorEnum.STORAGE_NOT_FOUND); + } + // 附件要批量插入 + await manager + .createQueryBuilder() + .relation(ProjectEntity, 'files') + .of(id) + .add(fileIds); + } + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + // 比较重要,做逻辑删除 + await this.projectRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个信息 + */ + async info(id: number) { + const info = await this.projectRepository + .createQueryBuilder('project') + .where({ + id + }) + .andWhere('project.isDelete = 0') + .getOne(); + return info; + } + + /** + * 解除附件关联 + * @param id 实体ID + * @param fileIds 附件ID + */ + async unlinkAttachments(id: number, fileIds: number[]) { + await this.entityManager.transaction(async manager => { + const project = await this.projectRepository + .createQueryBuilder('project') + .leftJoinAndSelect('project.files', 'files') + .where('project.id = :id', { id }) + .getOne(); + const linkedFiles = project.files + .map(item => item.id) + .filter(item => !fileIds.includes(item)); + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(ProjectEntity, 'files') + .of(id) + .addAndRemove(linkedFiles, project.files); + }); + } +} diff --git a/src/modules/sale_quotation/component/sale_quotation_component.controller.ts b/src/modules/sale_quotation/component/sale_quotation_component.controller.ts new file mode 100644 index 0000000..76d1fa5 --- /dev/null +++ b/src/modules/sale_quotation/component/sale_quotation_component.controller.ts @@ -0,0 +1,69 @@ +import { Body, Controller, Get, Query, Put, Delete, Post } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { SaleQuotationComponentDto, SaleQuotationComponentQueryDto, SaleQuotationComponentUpdateDto } from './sale_quotation_component.dto'; +import { SaleQuotationComponentService } from './sale_quotation_component.service'; +export const permissions = definePermission('sale_quotation:sale_quotation_component', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('SaleQuotationComponent - 报价配件') +@ApiSecurityAuth() +@Controller('sale_quotation_component') +export class SaleQuotationComponentController { + constructor(private saleQuotationComponentService: SaleQuotationComponentService) {} + + @Get() + @ApiOperation({ summary: '分页获取报价配件列表' }) + @ApiResult({ type: [SaleQuotationComponentDto], isPage: true }) + @Perm(permissions.LIST) + async list(@Query() dto: SaleQuotationComponentQueryDto) { + return this.saleQuotationComponentService.findAll(dto); + } + + @Get(':id') + @ApiOperation({ summary: '获取报价配件信息' }) + @ApiResult({ type: SaleQuotationComponentDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.saleQuotationComponentService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增报价配件' }) + @Perm(permissions.CREATE) + async create(@Body() dto: SaleQuotationComponentDto): Promise { + await this.saleQuotationComponentService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新报价配件' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: SaleQuotationComponentUpdateDto): Promise { + await this.saleQuotationComponentService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除报价配件' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.saleQuotationComponentService.delete(id); + } + + @Put('unlink-attachments/:id') + @ApiOperation({ summary: '附件解除关联' }) + @Perm(permissions.UPDATE) + async unlinkAttachments( + @IdParam() id: number, + @Body() { fileIds }: SaleQuotationComponentUpdateDto + ): Promise { + await this.saleQuotationComponentService.unlinkAttachments(id, fileIds); + } +} diff --git a/src/modules/sale_quotation/component/sale_quotation_component.dto.ts b/src/modules/sale_quotation/component/sale_quotation_component.dto.ts new file mode 100644 index 0000000..f0c6e94 --- /dev/null +++ b/src/modules/sale_quotation/component/sale_quotation_component.dto.ts @@ -0,0 +1,59 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { IsUnique } from '~/shared/database/constraints/unique.constraint'; +import { SaleQuotationComponentEntity } from './sale_quotation_component.entity'; + +export class SaleQuotationComponentDto { + @ApiProperty({ description: '报价配件名称' }) + @IsString() + name: string; + + @ApiProperty({ description: '配件规格' }) + @IsOptional() + @IsString() + componentSpecification: string; + + @ApiProperty({ description: '配件备注' }) + @IsOptional() + @IsString() + remark: string; + + @ApiProperty({ description: '单位(字典)' }) + @IsOptional() + @IsNumber() + unitId: number; + + @ApiProperty({ description: '单价' }) + @IsOptional() + @IsNumber() + unitPrice: number; + + @ApiProperty({ description: '附件' }) + files: Storage[]; +} + +export class SaleQuotationComponentUpdateDto extends PartialType(SaleQuotationComponentDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} + +export class ComapnyCreateDto extends PartialType(SaleQuotationComponentDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} + +export class SaleQuotationComponentQueryDto extends IntersectionType( + PagerDto, + PartialType(SaleQuotationComponentDto) +) { + @ApiProperty({ description: '报价配件名称' }) + @IsOptional() + @IsString() + name: string; +} diff --git a/src/modules/sale_quotation/component/sale_quotation_component.entity.ts b/src/modules/sale_quotation/component/sale_quotation_component.entity.ts new file mode 100644 index 0000000..5834376 --- /dev/null +++ b/src/modules/sale_quotation/component/sale_quotation_component.entity.ts @@ -0,0 +1,85 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { + Column, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, + Relation +} from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { SaleQuotationGroupEntity } from '../group/sale_quotation_group.entity'; +import { DictItemEntity } from '~/modules/system/dict-item/dict-item.entity'; + +/** + * 报价配件实体类 + */ +@Entity({ name: 'sale_quotation_component' }) +export class SaleQuotationComponentEntity extends CommonEntity { + @Column({ + name: 'name', + type: 'varchar', + length: 255, + comment: '报价配件名称' + }) + @ApiProperty({ description: '报价配件名称' }) + name: string; + + @Column({ + name: 'component_specification', + type: 'varchar', + nullable: true, + length: 255, + comment: '产品规格' + }) + @ApiProperty({ description: '产品规格', nullable: true }) + componentSpecification?: string; + + @Column({ name: 'unit_id', type: 'int', comment: '单位(字典)', nullable: true }) + @ApiProperty({ description: '单位(字典)' }) + unitId: number; + + @ManyToOne(() => DictItemEntity) + @JoinColumn({ name: 'unit_id' }) + unit: DictItemEntity; + + @Column({ + name: 'unit_price', + type: 'decimal', + precision: 15, + default: 0, + scale: 10, + comment: '单价' + }) + @ApiProperty({ description: '单价' }) + unitPrice: number; + + @Column({ + name: 'remark', + type: 'varchar', + length: 255, + nullable: true, + comment: '备注' + }) + @ApiProperty({ description: '产品备注' }) + remark: string; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; + + @ManyToMany(() => Storage, storage => storage.saleQuotationComponents) + @JoinTable({ + name: 'sale_quotation_component_storage', + joinColumn: { name: 'sale_quotation_component_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } + }) + files: Relation; + + @ApiHideProperty() + @ManyToMany(() => SaleQuotationGroupEntity, group => group.components) + groups: Relation; +} diff --git a/src/modules/sale_quotation/component/sale_quotation_component.module.ts b/src/modules/sale_quotation/component/sale_quotation_component.module.ts new file mode 100644 index 0000000..5954eac --- /dev/null +++ b/src/modules/sale_quotation/component/sale_quotation_component.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StorageModule } from '~/modules/tools/storage/storage.module'; +import { DatabaseModule } from '~/shared/database/database.module'; +import { SaleQuotationComponentService } from './sale_quotation_component.service'; +import { SaleQuotationComponentController } from './sale_quotation_component.controller'; +import { SaleQuotationComponentEntity } from './sale_quotation_component.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([SaleQuotationComponentEntity]), StorageModule, DatabaseModule], + controllers: [SaleQuotationComponentController], + providers: [SaleQuotationComponentService] +}) +export class SaleQuotationComponentModule {} diff --git a/src/modules/sale_quotation/component/sale_quotation_component.service.ts b/src/modules/sale_quotation/component/sale_quotation_component.service.ts new file mode 100644 index 0000000..4fd80a9 --- /dev/null +++ b/src/modules/sale_quotation/component/sale_quotation_component.service.ts @@ -0,0 +1,161 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, Not, Repository } from 'typeorm'; +import { Pagination } from '~/helper/paginate/pagination'; +import { paginate } from '~/helper/paginate'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { fieldSearch } from '~/shared/database/field-search'; +import { SaleQuotationComponentEntity } from './sale_quotation_component.entity'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { + SaleQuotationComponentDto, + SaleQuotationComponentQueryDto, + SaleQuotationComponentUpdateDto +} from './sale_quotation_component.dto'; + +@Injectable() +export class SaleQuotationComponentService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(SaleQuotationComponentEntity) + private saleQuotationComponentRepository: Repository, + @InjectRepository(Storage) + private storageRepository: Repository + ) {} + + buildSearchQuery() { + return this.saleQuotationComponentRepository + .createQueryBuilder('saleQuotationComponent') + .leftJoin('saleQuotationComponent.unit', 'unit') + .leftJoin('saleQuotationComponent.files', 'files') + .addSelect(['files.id', 'files.path', 'unit.id', 'unit.label']); + } + + /** + * 分页查询所有 + */ + async findAll({ + page, + pageSize, + ...fields + }: SaleQuotationComponentQueryDto): Promise> { + const queryBuilder = this.buildSearchQuery() + .where(fieldSearch(fields)) + .andWhere('saleQuotationComponent.isDelete = 0'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create(dto: SaleQuotationComponentDto): Promise { + const { unitPrice, name, componentSpecification } = dto; + const isExist = await this.saleQuotationComponentRepository.exist({ + where: { + unitPrice, + name, + componentSpecification + } + }); + if (isExist) { + throw new BusinessException(ErrorEnum.SALE_QUOTATION_COMPONENT_DUPLICATED); + } + await this.saleQuotationComponentRepository.insert(dto); + } + + /** + * 更新 + */ + async update( + id: number, + { fileIds, ...data }: Partial + ): Promise { + await this.entityManager.transaction(async manager => { + const beUpdateEntity = await manager.findOne(SaleQuotationComponentEntity, { + where: { + id + } + }); + const { unitPrice, name, componentSpecification } = beUpdateEntity; + const isExist = await this.saleQuotationComponentRepository.exist({ + where: { + id: Not(id), + unitPrice, + name, + componentSpecification + } + }); + if (isExist) { + throw new BusinessException(ErrorEnum.SALE_QUOTATION_COMPONENT_DUPLICATED); + } + await manager.update(SaleQuotationComponentEntity, id, { + ...data + }); + if (fileIds?.length) { + const count = await this.storageRepository + .createQueryBuilder('storage') + .where('storage.id in(:fileIds)', { fileIds }) + .getCount(); + if (count !== fileIds?.length) { + throw new BusinessException(ErrorEnum.STORAGE_NOT_FOUND); + } + // 附件要批量插入 + await manager + .createQueryBuilder() + .relation(SaleQuotationComponentEntity, 'files') + .of(id) + .add(fileIds); + } + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + // 比较重要,做逻辑删除 + await this.saleQuotationComponentRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个信息 + */ + async info(id: number) { + const info = await this.buildSearchQuery() + .where({ + id + }) + .andWhere('saleQuotationComponent.isDelete = 0') + .getOne(); + return info; + } + + /** + * 解除附件关联 + * @param id 实体ID + * @param fileIds 附件ID + */ + async unlinkAttachments(id: number, fileIds: number[]) { + await this.entityManager.transaction(async manager => { + const saleQuotationComponent = await this.saleQuotationComponentRepository + .createQueryBuilder('saleQuotationComponent') + .leftJoinAndSelect('saleQuotationComponent.files', 'files') + .where('saleQuotationComponent.id = :id', { id }) + .getOne(); + const linkedFiles = saleQuotationComponent.files + .map(item => item.id) + .filter(item => !fileIds.includes(item)); + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(SaleQuotationComponentEntity, 'files') + .of(id) + .addAndRemove(linkedFiles, saleQuotationComponent.files); + }); + } +} diff --git a/src/modules/sale_quotation/group/sale_quotation_group.controller.ts b/src/modules/sale_quotation/group/sale_quotation_group.controller.ts new file mode 100644 index 0000000..ebf1e2b --- /dev/null +++ b/src/modules/sale_quotation/group/sale_quotation_group.controller.ts @@ -0,0 +1,63 @@ +import { Body, Controller, Get, Query, Put, Delete, Post } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { + SaleQuotationGroupDto, + SaleQuotationGroupQueryDto, + SaleQuotationGroupUpdateDto +} from './sale_quotation_group.dto'; +import { SaleQuotationGroupService } from './sale_quotation_group.service'; +export const permissions = definePermission('sale_quotation:sale_quotation_group', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('SaleQuotationGroup - 报价分组') +@ApiSecurityAuth() +@Controller('sale_quotation_group') +export class SaleQuotationGroupController { + constructor(private saleQuotationGroupService: SaleQuotationGroupService) {} + + @Get() + @ApiOperation({ summary: '分页获取报价分组列表' }) + @ApiResult({ type: [SaleQuotationGroupDto], isPage: true }) + @Perm(permissions.LIST) + async list(@Query() dto: SaleQuotationGroupQueryDto) { + return this.saleQuotationGroupService.findAll(dto); + } + + @Get(':id') + @ApiOperation({ summary: '获取报价分组信息' }) + @ApiResult({ type: SaleQuotationGroupDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.saleQuotationGroupService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增报价分组' }) + @Perm(permissions.CREATE) + async create(@Body() dto: SaleQuotationGroupDto): Promise { + await this.saleQuotationGroupService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新报价分组' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: SaleQuotationGroupUpdateDto): Promise { + await this.saleQuotationGroupService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除报价分组' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.saleQuotationGroupService.delete(id); + } +} diff --git a/src/modules/sale_quotation/group/sale_quotation_group.dto.ts b/src/modules/sale_quotation/group/sale_quotation_group.dto.ts new file mode 100644 index 0000000..9b5d6f8 --- /dev/null +++ b/src/modules/sale_quotation/group/sale_quotation_group.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { IsUnique } from '~/shared/database/constraints/unique.constraint'; +import { SaleQuotationGroupEntity } from './sale_quotation_group.entity'; + +export class SaleQuotationGroupDto { + @ApiProperty({ description: '报价分组名称' }) + @IsUnique(SaleQuotationGroupEntity, { message: '已存在同名报价分组' }) + @IsString() + name: string; +} + +export class SaleQuotationGroupUpdateDto extends PartialType(SaleQuotationGroupDto) {} + +export class ComapnyCreateDto extends PartialType(SaleQuotationGroupDto) {} + +export class SaleQuotationGroupQueryDto extends IntersectionType( + PagerDto, + PartialType(SaleQuotationGroupDto) +) { + @ApiProperty({ description: '报价分组名称' }) + @IsOptional() + @IsString() + name: string; +} diff --git a/src/modules/sale_quotation/group/sale_quotation_group.entity.ts b/src/modules/sale_quotation/group/sale_quotation_group.entity.ts new file mode 100644 index 0000000..6d8276b --- /dev/null +++ b/src/modules/sale_quotation/group/sale_quotation_group.entity.ts @@ -0,0 +1,35 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, Relation } from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { SaleQuotationComponentEntity } from '../component/sale_quotation_component.entity'; + +/** + * 报价分组实体类 + */ +@Entity({ name: 'sale_quotation_group' }) +export class SaleQuotationGroupEntity extends CommonEntity { + @Column({ + name: 'name', + type: 'varchar', + unique: true, + length: 255, + comment: '报价分组名称' + }) + @ApiProperty({ description: '报价分组名称' }) + name: string; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; + + items:any[]; + + @ManyToMany(() => SaleQuotationComponentEntity, component => component.groups) + @JoinTable({ + name: 'sale_quotation_group_component', + joinColumn: { name: 'group_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'component_id', referencedColumnName: 'id' } + }) + components: Relation; +} diff --git a/src/modules/sale_quotation/group/sale_quotation_group.module.ts b/src/modules/sale_quotation/group/sale_quotation_group.module.ts new file mode 100644 index 0000000..a5cc146 --- /dev/null +++ b/src/modules/sale_quotation/group/sale_quotation_group.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StorageModule } from '~/modules/tools/storage/storage.module'; +import { DatabaseModule } from '~/shared/database/database.module'; +import { SaleQuotationGroupService } from './sale_quotation_group.service'; +import { SaleQuotationGroupController } from './sale_quotation_group.controller'; +import { SaleQuotationGroupEntity } from './sale_quotation_group.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([SaleQuotationGroupEntity]), DatabaseModule], + controllers: [SaleQuotationGroupController], + providers: [SaleQuotationGroupService] +}) +export class SaleQuotationGroupModule {} diff --git a/src/modules/sale_quotation/group/sale_quotation_group.service.ts b/src/modules/sale_quotation/group/sale_quotation_group.service.ts new file mode 100644 index 0000000..25fc1af --- /dev/null +++ b/src/modules/sale_quotation/group/sale_quotation_group.service.ts @@ -0,0 +1,83 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, Repository } from 'typeorm'; +import { Pagination } from '~/helper/paginate/pagination'; +import { paginate } from '~/helper/paginate'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { fieldSearch } from '~/shared/database/field-search'; +import { SaleQuotationGroupEntity } from './sale_quotation_group.entity'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { + SaleQuotationGroupDto, + SaleQuotationGroupQueryDto, + SaleQuotationGroupUpdateDto +} from './sale_quotation_group.dto'; + +@Injectable() +export class SaleQuotationGroupService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(SaleQuotationGroupEntity) + private saleQuotationGroupRepository: Repository + ) {} + + /** + * 分页查询所有 + */ + async findAll({ + page, + pageSize, + ...fields + }: SaleQuotationGroupQueryDto): Promise> { + const queryBuilder = this.saleQuotationGroupRepository + .createQueryBuilder('saleQuotationGroup') + .where(fieldSearch(fields)) + .andWhere('saleQuotationGroup.isDelete = 0'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create(dto: SaleQuotationGroupDto): Promise { + await this.saleQuotationGroupRepository.insert(dto); + } + + /** + * 更新 + */ + async update(id: number, data: Partial): Promise { + await this.entityManager.transaction(async manager => { + await manager.update(SaleQuotationGroupEntity, id, { + ...data + }); + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + // 比较重要,做逻辑删除 + await this.saleQuotationGroupRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个信息 + */ + async info(id: number) { + const info = await this.saleQuotationGroupRepository + .createQueryBuilder('saleQuotationGroup') + .where({ + id + }) + .andWhere('saleQuotationGroup.isDelete = 0') + .getOne(); + return info; + } +} diff --git a/src/modules/sale_quotation/sale_quotation.controller.ts b/src/modules/sale_quotation/sale_quotation.controller.ts new file mode 100644 index 0000000..9e16122 --- /dev/null +++ b/src/modules/sale_quotation/sale_quotation.controller.ts @@ -0,0 +1,24 @@ +import { Controller, Get, Query, Res } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { FastifyReply } from 'fastify'; +import { SaleQuotationService } from './sale_quotation.service'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +export const permissions = definePermission('sale_quotation:sale_quotation', { + EXPORT: 'export' +} as const); + +@ApiTags('SaleQuotation - 报价模块') +@ApiSecurityAuth() +@Controller('sale_quotation') +export class SaleQuotationController { + constructor(private saleQuotationService: SaleQuotationService) {} + + @Get('export/:id') + @ApiOperation({ summary: '导出报价配置明细' }) + @Perm(permissions.EXPORT) + async export(@IdParam() id: number, @Res() res: FastifyReply): Promise { + await this.saleQuotationService.export(id, res); + } +} diff --git a/src/modules/sale_quotation/sale_quotation.module.ts b/src/modules/sale_quotation/sale_quotation.module.ts new file mode 100644 index 0000000..f997cac --- /dev/null +++ b/src/modules/sale_quotation/sale_quotation.module.ts @@ -0,0 +1,32 @@ +import { Module } from '@nestjs/common'; +import { SaleQuotationGroupModule } from './group/sale_quotation_group.module'; +import { SaleQuotationTemplateModule } from './template/sale_quotation_template.module'; +import { SaleQuotationComponentModule } from './component/sale_quotation_component.module'; +import { RouterModule } from '@nestjs/core'; +import { SaleQuotationController } from './sale_quotation.controller'; +import { SaleQuotationService } from './sale_quotation.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SaleQuotationTemplateEntity } from './template/sale_quotation_template.entity'; +import { SaleQuotationGroupEntity } from './group/sale_quotation_group.entity'; +import { SaleQuotationComponentEntity } from './component/sale_quotation_component.entity'; +const modules = [ + SaleQuotationComponentModule, + SaleQuotationGroupModule, + SaleQuotationTemplateModule +]; +@Module({ + imports: [ + ...modules, + TypeOrmModule.forFeature([SaleQuotationTemplateEntity, SaleQuotationGroupEntity,SaleQuotationComponentEntity]), + RouterModule.register([ + { + path: 'sale_quotation', + module: SaleQuotationModule, + children: [...modules] + } + ]) + ], + controllers: [SaleQuotationController], + providers: [SaleQuotationService] +}) +export class SaleQuotationModule {} diff --git a/src/modules/sale_quotation/sale_quotation.service.ts b/src/modules/sale_quotation/sale_quotation.service.ts new file mode 100644 index 0000000..d9aca58 --- /dev/null +++ b/src/modules/sale_quotation/sale_quotation.service.ts @@ -0,0 +1,111 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, Repository } from 'typeorm'; +import { SaleQuotationTemplateEntity } from './template/sale_quotation_template.entity'; +import { FastifyReply } from 'fastify'; +import * as ExcelJS from 'exceljs'; +import { SaleQuotationGroupEntity } from './group/sale_quotation_group.entity'; +@Injectable() +export class SaleQuotationService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(SaleQuotationTemplateEntity) + private saleQuotationTemplateRepository: Repository + ) {} + + /** + * 导出报价配置明细 + */ + async export(templateId: number, res: FastifyReply): Promise { + const ROW_HEIGHT = 20; + const HEADER_FONT_SIZE = 18; + const workbook = new ExcelJS.Workbook(); + let data = await this.saleQuotationTemplateRepository.findOneBy({ id: templateId }); + const template: JSON = data.template; + const sheet = workbook.addWorksheet(data.name); + if (template != null) { + sheet.mergeCells('A1:H1'); + // 设置标题 + sheet.getCell('A1').value = '电液控部分配置明细'; + // 设置副标题 + sheet.mergeCells('A2:F2'); + sheet.getCell('A2').value = '支架电液控系统配置明细(中间131架+过渡架4架)'; + sheet.getCell(`G2`).value = ''; + sheet.getCell(`H2`).value = ''; + const groups = template['data'] as SaleQuotationGroupEntity[]; + const headers = [ + '过渡', + '名称', + '规格、型号及说明', + '单位', + '数量', + '备 注', + '单价', + '总价' + ]; + + sheet.addRow(headers); + for (let i = 1; i <= 8; i++) { + sheet.getCell(`${String.fromCharCode(64 + i)}1`).style.font = { bold: true }; + sheet.getCell(`${String.fromCharCode(64 + i)}2`).style.font = { bold: true }; + sheet.getCell(`${String.fromCharCode(64 + i)}3`).style.font = { bold: true }; + } + + let rowIndex = 3; + for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) { + rowIndex++; + const group = groups[groupIndex]; + sheet.mergeCells(`A${rowIndex}:F${rowIndex}`); + sheet.getCell(`A${rowIndex}`).value = group.name; + sheet.getCell(`A${rowIndex}`).style.font = { bold: true }; + sheet.getCell(`G${rowIndex}`).value = ''; + sheet.getCell(`H${rowIndex}`).value = ''; + for (let componentIndex = 0; componentIndex < group.items.length; componentIndex++) { + const item = group.items[componentIndex]; + rowIndex++; + sheet.addRow([ + `${componentIndex + 1}`, + item.name ?? '', + item.componentSpecification ?? '', + item.unit ?? '', + item.quantity ?? '', + item.remark ?? '', + item.unitPrice ?? '', + item.amount ?? '' + ]); + } + } + sheet.getCell(`I${rowIndex - 1}`).value = '总价'; + sheet.getCell(`I${rowIndex - 1}`).style.font = { bold: true }; + sheet.getCell(`I${rowIndex}`).value = template['totalPrice']; + } + sheet.eachRow((row, index) => { + if (index >= 0) { + row.alignment = { vertical: 'middle', horizontal: 'center' }; + row.height = ROW_HEIGHT; + row.eachCell(cell => { + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + }; + }); + } + }); + const columnWidthMap = { A: 8, B: 30, C: 25, F: 20 }; + sheet.columns.forEach((column, index: number) => { + column.width = columnWidthMap[String.fromCharCode(65 + index)] ?? 10; // Minimum width of 10 + }); + + //读取buffer进行传输 + const buffer = await workbook.xlsx.writeBuffer(); + res + .header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + .header( + 'Content-Disposition', + `attachment; filename="${encodeURIComponent('导出_excel' + new Date().getTime() + '.xls')}"` + ) + .send(buffer); + } +} diff --git a/src/modules/sale_quotation/template/sale_quotation_template.controller.ts b/src/modules/sale_quotation/template/sale_quotation_template.controller.ts new file mode 100644 index 0000000..18a9d1a --- /dev/null +++ b/src/modules/sale_quotation/template/sale_quotation_template.controller.ts @@ -0,0 +1,63 @@ +import { Body, Controller, Get, Query, Put, Delete, Post } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { + SaleQuotationTemplateDto, + SaleQuotationTemplateQueryDto, + SaleQuotationTemplateUpdateDto +} from './sale_quotation_template.dto'; +import { SaleQuotationTemplateService } from './sale_quotation_template.service'; +export const permissions = definePermission('sale_quotation:sale_quotation_template', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('SaleQuotationTemplate - 报价模板') +@ApiSecurityAuth() +@Controller('sale_quotation_template') +export class SaleQuotationTemplateController { + constructor(private saleQuotationTemplateService: SaleQuotationTemplateService) {} + + @Get() + @ApiOperation({ summary: '分页获取报价模板列表' }) + @ApiResult({ type: [SaleQuotationTemplateDto], isPage: true }) + @Perm(permissions.LIST) + async list(@Query() dto: SaleQuotationTemplateQueryDto) { + return this.saleQuotationTemplateService.findAll(dto); + } + + @Get(':id') + @ApiOperation({ summary: '获取报价模板信息' }) + @ApiResult({ type: SaleQuotationTemplateDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.saleQuotationTemplateService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增报价模板' }) + @Perm(permissions.CREATE) + async create(@Body() dto: SaleQuotationTemplateDto): Promise { + await this.saleQuotationTemplateService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新报价模板' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: SaleQuotationTemplateUpdateDto): Promise { + await this.saleQuotationTemplateService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除报价模板' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.saleQuotationTemplateService.delete(id); + } +} diff --git a/src/modules/sale_quotation/template/sale_quotation_template.dto.ts b/src/modules/sale_quotation/template/sale_quotation_template.dto.ts new file mode 100644 index 0000000..b916fd7 --- /dev/null +++ b/src/modules/sale_quotation/template/sale_quotation_template.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { IsUnique } from '~/shared/database/constraints/unique.constraint'; +import { SaleQuotationTemplateEntity } from './sale_quotation_template.entity'; + +export class SaleQuotationTemplateDto { + @ApiProperty({ description: '报价模板名称' }) + @IsString() + name: string; + + @ApiProperty({ description: '报价模板' }) + @IsOptional() + template: JSON; +} + +export class SaleQuotationTemplateUpdateDto extends PartialType(SaleQuotationTemplateDto) {} + +export class ComapnyCreateDto extends PartialType(SaleQuotationTemplateDto) {} + +export class SaleQuotationTemplateQueryDto extends IntersectionType( + PagerDto, + PartialType(SaleQuotationTemplateDto) +) { + @ApiProperty({ description: '报价模板名称' }) + @IsOptional() + @IsString() + name: string; +} diff --git a/src/modules/sale_quotation/template/sale_quotation_template.entity.ts b/src/modules/sale_quotation/template/sale_quotation_template.entity.ts new file mode 100644 index 0000000..9e2aabe --- /dev/null +++ b/src/modules/sale_quotation/template/sale_quotation_template.entity.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Column, Entity } from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; + +/** + * 报价模板实体类 + */ +@Entity({ name: 'sale_quotation_template' }) +export class SaleQuotationTemplateEntity extends CommonEntity { + @Column({ + name: 'name', + type: 'varchar', + unique: true, + length: 255, + comment: '报价模板名称' + }) + @ApiProperty({ description: '报价模板名称' }) + name: string; + + @Column({ + name: 'template', + type: 'json', + comment: '报价模板', + nullable: true + }) + @ApiProperty({ description: '报价模板(JSON)' }) + template: JSON; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; +} diff --git a/src/modules/sale_quotation/template/sale_quotation_template.module.ts b/src/modules/sale_quotation/template/sale_quotation_template.module.ts new file mode 100644 index 0000000..01c4c03 --- /dev/null +++ b/src/modules/sale_quotation/template/sale_quotation_template.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StorageModule } from '~/modules/tools/storage/storage.module'; +import { DatabaseModule } from '~/shared/database/database.module'; +import { SaleQuotationTemplateService } from './sale_quotation_template.service'; +import { SaleQuotationTemplateController } from './sale_quotation_template.controller'; +import { SaleQuotationTemplateEntity } from './sale_quotation_template.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([SaleQuotationTemplateEntity]), DatabaseModule], + controllers: [SaleQuotationTemplateController], + providers: [SaleQuotationTemplateService], +}) +export class SaleQuotationTemplateModule {} diff --git a/src/modules/sale_quotation/template/sale_quotation_template.service.ts b/src/modules/sale_quotation/template/sale_quotation_template.service.ts new file mode 100644 index 0000000..dd7a83f --- /dev/null +++ b/src/modules/sale_quotation/template/sale_quotation_template.service.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, Not, Repository } from 'typeorm'; +import { Pagination } from '~/helper/paginate/pagination'; +import { paginate } from '~/helper/paginate'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { fieldSearch } from '~/shared/database/field-search'; +import { SaleQuotationTemplateEntity } from './sale_quotation_template.entity'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { + SaleQuotationTemplateDto, + SaleQuotationTemplateQueryDto, + SaleQuotationTemplateUpdateDto +} from './sale_quotation_template.dto'; + +@Injectable() +export class SaleQuotationTemplateService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(SaleQuotationTemplateEntity) + private saleQuotationTemplateRepository: Repository + ) {} + + /** + * 分页查询所有 + */ + async findAll({ + page, + pageSize, + ...fields + }: SaleQuotationTemplateQueryDto): Promise> { + const queryBuilder = this.saleQuotationTemplateRepository + .createQueryBuilder('saleQuotationTemplate') + .where(fieldSearch(fields)) + .andWhere('saleQuotationTemplate.isDelete = 0') + .addOrderBy('saleQuotationTemplate.createdAt', 'DESC'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create(dto: SaleQuotationTemplateDto): Promise { + const isDuplicated = await this.saleQuotationTemplateRepository.exist({ + where: { name: dto.name } + }); + if (isDuplicated) { + throw new BusinessException(ErrorEnum.SALE_QUOTATION_TEMPLATE_NAME_DUPLICATE); + } + await this.saleQuotationTemplateRepository.insert(dto); + } + + /** + * 更新 + */ + async update(id: number, data: Partial): Promise { + await this.entityManager.transaction(async manager => { + const isDuplicated = await this.saleQuotationTemplateRepository.exist({ + where: { name: data.name, id: Not(id) } + }); + if (isDuplicated) { + throw new BusinessException(ErrorEnum.SALE_QUOTATION_TEMPLATE_NAME_DUPLICATE); + } + await manager.update(SaleQuotationTemplateEntity, id, { + ...data + }); + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + // 比较重要,做逻辑删除 + await this.saleQuotationTemplateRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个信息 + */ + async info(id: number) { + const info = await this.saleQuotationTemplateRepository + .createQueryBuilder('saleQuotationTemplate') + .where({ + id + }) + .andWhere('saleQuotationTemplate.isDelete = 0') + .getOne(); + return info; + } +} diff --git a/src/modules/sse/sse.controller.ts b/src/modules/sse/sse.controller.ts new file mode 100644 index 0000000..e6c581f --- /dev/null +++ b/src/modules/sse/sse.controller.ts @@ -0,0 +1,66 @@ +import { + BeforeApplicationShutdown, + Controller, + Param, + ParseIntPipe, + Req, + Res, + Sse +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { FastifyReply, FastifyRequest } from 'fastify'; +import { Observable, interval } from 'rxjs'; + +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; + +import { MessageEvent, SseService } from './sse.service'; + +@ApiTags('System - sse模块') +@ApiSecurityAuth() +@Controller('sse') +export class SseController implements BeforeApplicationShutdown { + private replyMap: Map = new Map(); + + constructor(private readonly sseService: SseService) {} + + private closeAllConnect() { + this.sseService.sendToAll({ + type: 'close', + data: 'bye~' + }); + this.replyMap.forEach(reply => { + reply.raw.end().destroy(); + }); + } + + // 通过控制台关闭程序时触发 + beforeApplicationShutdown() { + // console.log('beforeApplicationShutdown') + this.closeAllConnect(); + } + + @Sse(':uid') + sse( + @Param('uid', ParseIntPipe) uid: number, + @Req() req: FastifyRequest, + @Res() res: FastifyReply + ): Observable { + this.replyMap.set(uid, res); + + const subscription = interval(10000).subscribe(() => { + this.sseService.sendToClient(uid, { type: 'ping' }); + }); + + // 当客户端断开连接时 + req.raw.on('close', () => { + subscription.unsubscribe(); + this.sseService.removeClient(uid); + this.replyMap.delete(uid); + // console.log(`user-${uid}已关闭`) + }); + + return new Observable(subscriber => { + this.sseService.addClient(uid, subscriber); + }); + } +} diff --git a/src/modules/sse/sse.module.ts b/src/modules/sse/sse.module.ts new file mode 100644 index 0000000..4185d1b --- /dev/null +++ b/src/modules/sse/sse.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; + +import { SseController } from './sse.controller'; +import { SseService } from './sse.service'; + +@Module({ + imports: [], + controllers: [SseController], + providers: [SseService], + exports: [SseService] +}) +export class SseModule {} diff --git a/src/modules/sse/sse.service.ts b/src/modules/sse/sse.service.ts new file mode 100644 index 0000000..cec6e37 --- /dev/null +++ b/src/modules/sse/sse.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@nestjs/common'; +import { Subscriber } from 'rxjs'; +import { In } from 'typeorm'; + +import { ROOT_ROLE_ID } from '~/constants/system.constant'; + +import { RoleEntity } from '~/modules/system/role/role.entity'; +import { UserEntity } from '~/modules/user/user.entity'; + +export interface MessageEvent { + data?: string | object; + id?: string; + type?: 'ping' | 'close' | 'updatePermsAndMenus'; + retry?: number; +} + +const clientMap: Map> = new Map(); + +@Injectable() +export class SseService { + addClient(uid: number, subscriber: Subscriber) { + clientMap.set(uid, subscriber); + } + + removeClient(uid: number): void { + const client = clientMap.get(uid); + client?.complete(); + clientMap.delete(uid); + } + + sendToClient(uid: number, data: MessageEvent): void { + const client = clientMap.get(uid); + client?.next?.(data); + } + + sendToAll(data: MessageEvent): void { + clientMap.forEach(client => { + client.next(data); + }); + } + + /** + * 通知前端重新获取权限菜单 + * @param uid + * @constructor + */ + async noticeClientToUpdateMenusByUserIds(uid: number | number[]) { + const userIds = [].concat(uid) as number[]; + userIds.forEach(uid => { + this.sendToClient(uid, { type: 'updatePermsAndMenus' }); + }); + } + + /** + * 通过menuIds通知用户更新权限菜单 + */ + async noticeClientToUpdateMenusByMenuIds(menuIds: number[]): Promise { + const roleMenus = await RoleEntity.find({ + where: { + menus: { + id: In(menuIds) + } + } + }); + const roleIds = roleMenus.map(n => n.id).concat(ROOT_ROLE_ID); + await this.noticeClientToUpdateMenusByRoleIds(roleIds); + } + + /** + * 通过roleIds通知用户更新权限菜单 + */ + async noticeClientToUpdateMenusByRoleIds(roleIds: number[]): Promise { + const users = await UserEntity.find({ + where: { + roles: { + id: In(roleIds) + } + } + }); + if (users) { + const userIds = users.map(n => n.id); + await this.noticeClientToUpdateMenusByUserIds(userIds); + } + } +} diff --git a/src/modules/system/dept/dept.controller.ts b/src/modules/system/dept/dept.controller.ts new file mode 100644 index 0000000..044c045 --- /dev/null +++ b/src/modules/system/dept/dept.controller.ts @@ -0,0 +1,79 @@ +import { Body, Controller, Delete, Get, Post, Put, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { AuthUser } from '~/modules/auth/decorators/auth-user.decorator'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { DeptEntity } from '~/modules/system/dept/dept.entity'; + +import { DeptDto, DeptQueryDto } from './dept.dto'; +import { DeptService } from './dept.service'; + +export const permissions = definePermission('system:dept', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiSecurityAuth() +@ApiTags('System - 部门模块') +@Controller('depts') +export class DeptController { + constructor(private deptService: DeptService) {} + + @Get() + @ApiOperation({ summary: '获取部门列表' }) + @ApiResult({ type: [DeptEntity] }) + @Perm(permissions.LIST) + async list(@Query() dto: DeptQueryDto, @AuthUser('uid') uid: number): Promise { + return this.deptService.getDeptTree(uid, dto); + } + + @Post() + @ApiOperation({ summary: '创建部门' }) + @Perm(permissions.CREATE) + async create(@Body() dto: DeptDto): Promise { + await this.deptService.create(dto); + } + + @Get(':id') + @ApiOperation({ summary: '查询部门信息' }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.deptService.info(id); + } + + @Put(':id') + @ApiOperation({ summary: '更新部门' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() updateDeptDto: DeptDto): Promise { + await this.deptService.update(id, updateDeptDto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除部门' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + // 查询是否有关联用户或者部门,如果含有则无法删除 + const count = await this.deptService.countUserByDeptId(id); + if (count > 0) throw new BusinessException(ErrorEnum.DEPARTMENT_HAS_ASSOCIATED_USERS); + + const count2 = await this.deptService.countChildDept(id); + console.log('count2', count2); + if (count2 > 0) throw new BusinessException(ErrorEnum.DEPARTMENT_HAS_CHILD_DEPARTMENTS); + + await this.deptService.delete(id); + } + + // @Post('move') + // @ApiOperation({ summary: '部门移动排序' }) + // async move(@Body() dto: MoveDeptDto): Promise { + // await this.deptService.move(dto.depts); + // } +} diff --git a/src/modules/system/dept/dept.dto.ts b/src/modules/system/dept/dept.dto.ts new file mode 100644 index 0000000..42d2507 --- /dev/null +++ b/src/modules/system/dept/dept.dto.ts @@ -0,0 +1,70 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + ArrayNotEmpty, + IsArray, + IsInt, + IsOptional, + IsString, + Min, + MinLength, + ValidateNested +} from 'class-validator'; + +export class DeptDto { + @ApiProperty({ description: '部门名称' }) + @IsString() + @MinLength(1) + name: string; + + @ApiProperty({ description: '父级部门id' }) + @Type(() => Number) + @IsInt() + @IsOptional() + parentId: number; + + @ApiProperty({ description: '排序编号', required: false }) + @IsInt() + @Min(0) + @IsOptional() + orderNo: number; +} + +export class TransferDeptDto { + @ApiProperty({ description: '需要转移的管理员列表编号', type: [Number] }) + @IsArray() + @ArrayNotEmpty() + userIds: number[]; + + @ApiProperty({ description: '需要转移过去的系统部门ID' }) + @IsInt() + @Min(0) + deptId: number; +} + +export class MoveDept { + @ApiProperty({ description: '当前部门ID' }) + @IsInt() + @Min(0) + id: number; + + @ApiProperty({ description: '移动到指定父级部门的ID' }) + @IsInt() + @Min(0) + @IsOptional() + parentId: number; +} + +export class MoveDeptDto { + @ApiProperty({ description: '部门列表', type: [MoveDept] }) + @ValidateNested({ each: true }) + @Type(() => MoveDept) + depts: MoveDept[]; +} + +export class DeptQueryDto { + @ApiProperty({ description: '部门名称' }) + @IsString() + @IsOptional() + name?: string; +} diff --git a/src/modules/system/dept/dept.entity.ts b/src/modules/system/dept/dept.entity.ts new file mode 100644 index 0000000..7092dbe --- /dev/null +++ b/src/modules/system/dept/dept.entity.ts @@ -0,0 +1,28 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, OneToMany, Relation, Tree, TreeChildren, TreeParent } from 'typeorm'; + +import { CommonEntity } from '~/common/entity/common.entity'; + +import { UserEntity } from '../../user/user.entity'; + +@Entity({ name: 'sys_dept' }) +@Tree('materialized-path') +export class DeptEntity extends CommonEntity { + @Column() + @ApiProperty({ description: '部门名称' }) + name: string; + + @Column({ nullable: true, default: 0 }) + @ApiProperty({ description: '排序' }) + orderNo: number; + + @TreeChildren({ cascade: true }) + children: DeptEntity[]; + + @TreeParent({ onDelete: 'SET NULL' }) + parent?: DeptEntity; + + @ApiHideProperty() + @OneToMany(() => UserEntity, user => user.dept) + users: Relation; +} diff --git a/src/modules/system/dept/dept.module.ts b/src/modules/system/dept/dept.module.ts new file mode 100644 index 0000000..6de2058 --- /dev/null +++ b/src/modules/system/dept/dept.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { UserModule } from '../../user/user.module'; +import { RoleModule } from '../role/role.module'; + +import { DeptController } from './dept.controller'; +import { DeptEntity } from './dept.entity'; +import { DeptService } from './dept.service'; + +const services = [DeptService]; + +@Module({ + imports: [TypeOrmModule.forFeature([DeptEntity]), UserModule, RoleModule], + controllers: [DeptController], + providers: [...services], + exports: [TypeOrmModule, ...services] +}) +export class DeptModule {} diff --git a/src/modules/system/dept/dept.service.ts b/src/modules/system/dept/dept.service.ts new file mode 100644 index 0000000..08f9cfa --- /dev/null +++ b/src/modules/system/dept/dept.service.ts @@ -0,0 +1,126 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { isEmpty } from 'lodash'; +import { EntityManager, Repository, TreeRepository } from 'typeorm'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { DeptEntity } from '~/modules/system/dept/dept.entity'; +import { UserEntity } from '~/modules/user/user.entity'; + +import { deleteEmptyChildren } from '~/utils/list2tree.util'; + +import { DeptDto, DeptQueryDto, MoveDept } from './dept.dto'; + +@Injectable() +export class DeptService { + constructor( + @InjectRepository(UserEntity) + private userRepository: Repository, + @InjectRepository(DeptEntity) + private deptRepository: TreeRepository, + @InjectEntityManager() private entityManager: EntityManager + ) {} + + async list(): Promise { + return this.deptRepository.find({ order: { orderNo: 'DESC' } }); + } + + async info(id: number): Promise { + const dept = await this.deptRepository + .createQueryBuilder('dept') + .leftJoinAndSelect('dept.parent', 'parent') + .where({ id }) + .getOne(); + + if (isEmpty(dept)) throw new BusinessException(ErrorEnum.DEPARTMENT_NOT_FOUND); + + return dept; + } + + async create({ parentId, ...data }: DeptDto): Promise { + const parent = await this.deptRepository + .createQueryBuilder('dept') + .where({ id: parentId }) + .getOne(); + + await this.deptRepository.save({ + ...data, + parent + }); + } + + async update(id: number, { parentId, ...data }: DeptDto): Promise { + const item = await this.deptRepository.createQueryBuilder('dept').where({ id }).getOne(); + + const parent = await this.deptRepository + .createQueryBuilder('dept') + .where({ id: parentId }) + .getOne(); + + await this.deptRepository.save({ + ...item, + ...data, + parent + }); + } + + async delete(id: number): Promise { + await this.deptRepository.delete(id); + } + + /** + * 移动排序 + */ + async move(depts: MoveDept[]): Promise { + await this.entityManager.transaction(async manager => { + await manager.save(depts); + }); + } + + /** + * 根据部门查询关联的用户数量 + */ + async countUserByDeptId(id: number): Promise { + return this.userRepository.countBy({ dept: { id } }); + } + + /** + * 查找当前部门下的子部门数量 + */ + async countChildDept(id: number): Promise { + const item = await this.deptRepository.findOneBy({ id }); + return (await this.deptRepository.countDescendants(item)) - 1; + } + + /** + * 获取部门列表树结构 + */ + async getDeptTree(uid: number, { name }: DeptQueryDto): Promise { + const tree: DeptEntity[] = []; + + if (name) { + const deptList = await this.deptRepository + .createQueryBuilder('dept') + .where('dept.name like :name', { name: `%${name}%` }) + .getMany(); + + for (const dept of deptList) { + const deptTree = await this.deptRepository.findDescendantsTree(dept); + tree.push(deptTree); + } + + deleteEmptyChildren(tree); + + return tree; + } + + const deptTree = await this.deptRepository.findTrees({ + relations: ['parent'] + }); + + deleteEmptyChildren(deptTree); + + return deptTree; + } +} diff --git a/src/modules/system/dict-item/dict-item.controller.ts b/src/modules/system/dict-item/dict-item.controller.ts new file mode 100644 index 0000000..d203265 --- /dev/null +++ b/src/modules/system/dict-item/dict-item.controller.ts @@ -0,0 +1,68 @@ +import { Body, Controller, Delete, Get, Param, Post, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { Pagination } from '~/helper/paginate/pagination'; +import { AuthUser } from '~/modules/auth/decorators/auth-user.decorator'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { DictItemEntity } from '~/modules/system/dict-item/dict-item.entity'; + +import { DictItemDto, DictItemQueryDto } from './dict-item.dto'; +import { DictItemService } from './dict-item.service'; + +export const permissions = definePermission('system:dict-item', { + CREATE: 'create', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('System - 字典项模块') +@ApiSecurityAuth() +@Controller('dict-item') +export class DictItemController { + constructor(private dictItemService: DictItemService) {} + + @Get() + @ApiOperation({ summary: '获取字典项列表' }) + @ApiResult({ type: [DictItemEntity], isPage: true }) + async list(@Query() dto: DictItemQueryDto): Promise> { + return this.dictItemService.page(dto); + } + + @Get('all/:typeId') + @ApiOperation({ summary: '一次性通过字典类型获取所有所属的字典项(不分页)' }) + @ApiResult({ type: [DictItemEntity] }) + async getAll(@Param('typeId') typeId: number): Promise { + return this.dictItemService.getAllByType(typeId); + } + + @Post() + @ApiOperation({ summary: '新增字典项' }) + @Perm(permissions.CREATE) + async create(@Body() dto: DictItemDto, @AuthUser() user: IAuthUser): Promise { + await this.dictItemService.isExistKey(dto); + dto.createBy = dto.updateBy = user.uid; + await this.dictItemService.create(dto); + } + + @Post(':id') + @ApiOperation({ summary: '更新字典项' }) + @Perm(permissions.UPDATE) + async update( + @IdParam() id: number, + @Body() dto: DictItemDto, + @AuthUser() user: IAuthUser + ): Promise { + dto.updateBy = user.uid; + await this.dictItemService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除指定的字典项' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.dictItemService.delete(id); + } +} diff --git a/src/modules/system/dict-item/dict-item.dto.ts b/src/modules/system/dict-item/dict-item.dto.ts new file mode 100644 index 0000000..5bcd7d5 --- /dev/null +++ b/src/modules/system/dict-item/dict-item.dto.ts @@ -0,0 +1,48 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { IsInt, IsOptional, IsString, MinLength } from 'class-validator'; + +import { PagerDto } from '~/common/dto/pager.dto'; + +import { DictItemEntity } from './dict-item.entity'; + +export class DictItemDto extends PartialType(DictItemEntity) { + @ApiProperty({ description: '字典类型 ID' }) + @IsInt() + typeId: number; + + @ApiProperty({ description: '字典项键名' }) + @IsString() + @MinLength(1) + label: string; + + @ApiProperty({ description: '字典项值' }) + @IsString() + @MinLength(1) + value: string; + + @ApiProperty({ description: '状态' }) + @IsOptional() + @IsInt() + status?: number; + + @ApiProperty({ description: '备注' }) + @IsOptional() + @IsString() + remark?: string; +} + +export class DictItemQueryDto extends PagerDto { + @ApiProperty({ description: '字典类型 ID', required: true }) + @IsInt() + typeId: number; + + @ApiProperty({ description: '字典项键名' }) + @IsString() + @IsOptional() + label?: string; + + @ApiProperty({ description: '字典项值' }) + @IsString() + @IsOptional() + value?: string; +} diff --git a/src/modules/system/dict-item/dict-item.entity.ts b/src/modules/system/dict-item/dict-item.entity.ts new file mode 100644 index 0000000..523f4d0 --- /dev/null +++ b/src/modules/system/dict-item/dict-item.entity.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; + +import { CompleteEntity } from '~/common/entity/common.entity'; + +import { DictTypeEntity } from '../dict-type/dict-type.entity'; + +@Entity({ name: 'sys_dict_item' }) +export class DictItemEntity extends CompleteEntity { + @ManyToOne(() => DictTypeEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'type_id' }) + type: DictTypeEntity; + + @Column({ type: 'varchar', length: 50 }) + @ApiProperty({ description: '字典项键名' }) + label: string; + + @Column({ type: 'varchar', length: 50 }) + @ApiProperty({ description: '字典项值' }) + value: string; + + @Column({ nullable: true, comment: '字典项排序' }) + orderNo: number; + + @Column({ type: 'tinyint', default: 1 }) + @ApiProperty({ description: ' 状态' }) + status: number; + + @Column({ type: 'varchar', nullable: true }) + @ApiProperty({ description: '备注' }) + remark: string; +} diff --git a/src/modules/system/dict-item/dict-item.module.ts b/src/modules/system/dict-item/dict-item.module.ts new file mode 100644 index 0000000..4929562 --- /dev/null +++ b/src/modules/system/dict-item/dict-item.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { DictItemController } from './dict-item.controller'; +import { DictItemEntity } from './dict-item.entity'; +import { DictItemService } from './dict-item.service'; + +const services = [DictItemService]; + +@Module({ + imports: [TypeOrmModule.forFeature([DictItemEntity])], + controllers: [DictItemController], + providers: [...services], + exports: [TypeOrmModule, ...services] +}) +export class DictItemModule {} diff --git a/src/modules/system/dict-item/dict-item.service.ts b/src/modules/system/dict-item/dict-item.service.ts new file mode 100644 index 0000000..2ad8221 --- /dev/null +++ b/src/modules/system/dict-item/dict-item.service.ts @@ -0,0 +1,102 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Like, Repository } from 'typeorm'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { paginate } from '~/helper/paginate'; +import { Pagination } from '~/helper/paginate/pagination'; +import { DictItemEntity } from '~/modules/system/dict-item/dict-item.entity'; + +import { DictItemDto, DictItemQueryDto } from './dict-item.dto'; + +@Injectable() +export class DictItemService { + constructor( + @InjectRepository(DictItemEntity) + private dictItemRepository: Repository + ) {} + + /** + * 罗列所有配置 + */ + async page({ + page, + pageSize, + label, + value, + typeId + }: DictItemQueryDto): Promise> { + const queryBuilder = this.dictItemRepository + .createQueryBuilder('dict_item') + .orderBy({ orderNo: 'ASC' }) + .where({ + ...(label && { label: Like(`%${label}%`) }), + ...(value && { value: Like(`%${value}%`) }), + type: { + id: typeId + } + }); + + return paginate(queryBuilder, { page, pageSize }); + } + + /** 一次性获取所有的字典项 */ + async getAllByType(typeId: number) { + return this.dictItemRepository.find({ where: { type: { id: typeId } } }); + } + + /** + * 获取参数总数 + */ + async countConfigList(): Promise { + return this.dictItemRepository.count(); + } + + /** + * 新增 + */ + async create(dto: DictItemDto): Promise { + const { typeId, ...rest } = dto; + await this.dictItemRepository.insert({ + ...rest, + type: { + id: typeId + } + }); + } + + /** + * 更新 + */ + async update(id: number, dto: Partial): Promise { + const { typeId, ...rest } = dto; + await this.dictItemRepository.update(id, { + ...rest, + type: { + id: typeId + } + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + await this.dictItemRepository.delete(id); + } + + /** + * 查询单个 + */ + async findOne(id: number): Promise { + return this.dictItemRepository.findOneBy({ id }); + } + + async isExistKey(dto: DictItemDto): Promise { + const { value, typeId } = dto; + const result = await this.dictItemRepository.findOneBy({ value, type: { id: typeId } }); + if (result) throw new BusinessException(ErrorEnum.DICT_NAME_EXISTS); + } +} diff --git a/src/modules/system/dict-type/dict-type.controller.ts b/src/modules/system/dict-type/dict-type.controller.ts new file mode 100644 index 0000000..4f54a55 --- /dev/null +++ b/src/modules/system/dict-type/dict-type.controller.ts @@ -0,0 +1,75 @@ +import { Body, Controller, Delete, Get, Post, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { Pagination } from '~/helper/paginate/pagination'; +import { AuthUser } from '~/modules/auth/decorators/auth-user.decorator'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { DictTypeEntity } from '~/modules/system/dict-type/dict-type.entity'; + +import { DictTypeDto, DictTypeQueryDto } from './dict-type.dto'; +import { DictTypeService } from './dict-type.service'; + +export const permissions = definePermission('system:dict-type', { + CREATE: 'create', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('System - 字典类型模块') +@ApiSecurityAuth() +@Controller('dict-type') +export class DictTypeController { + constructor(private dictTypeService: DictTypeService) {} + + @Get() + @ApiOperation({ summary: '获取字典类型列表' }) + @ApiResult({ type: [DictTypeEntity], isPage: true }) + async list(@Query() dto: DictTypeQueryDto): Promise> { + return this.dictTypeService.page(dto); + } + + @Post('all') + @ApiOperation({ summary: '一次性获取所有的字典类型(不分页)' }) + @ApiResult({ type: [DictTypeEntity] }) + async getAll(@Body() dto: DictTypeQueryDto): Promise { + return this.dictTypeService.getAll(dto); + } + + @Post() + @ApiOperation({ summary: '新增字典类型' }) + @Perm(permissions.CREATE) + async create(@Body() dto: DictTypeDto, @AuthUser() user: IAuthUser): Promise { + await this.dictTypeService.isExistKey(dto.name); + dto.createBy = dto.updateBy = user.uid; + await this.dictTypeService.create(dto); + } + + @Get(':id') + @ApiOperation({ summary: '查询字典类型信息' }) + @ApiResult({ type: DictTypeEntity }) + async info(@IdParam() id: number): Promise { + return this.dictTypeService.findOne(id); + } + + @Post(':id') + @ApiOperation({ summary: '更新字典类型' }) + @Perm(permissions.UPDATE) + async update( + @IdParam() id: number, + @Body() dto: DictTypeDto, + @AuthUser() user: IAuthUser + ): Promise { + dto.updateBy = user.uid; + await this.dictTypeService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除指定的字典类型' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.dictTypeService.delete(id); + } +} diff --git a/src/modules/system/dict-type/dict-type.dto.ts b/src/modules/system/dict-type/dict-type.dto.ts new file mode 100644 index 0000000..9ee5771 --- /dev/null +++ b/src/modules/system/dict-type/dict-type.dto.ts @@ -0,0 +1,51 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { IsArray, IsBoolean, IsInt, IsOptional, IsString, MinLength } from 'class-validator'; + +import { PagerDto } from '~/common/dto/pager.dto'; + +import { DictTypeEntity } from './dict-type.entity'; +import { isBoolean } from 'lodash'; + +export class DictTypeDto extends PartialType(DictTypeEntity) { + @ApiProperty({ description: '字典类型名称' }) + @IsString() + @MinLength(1) + name: string; + + @ApiProperty({ description: '字典类型code' }) + @IsString() + @MinLength(3) + code: string; + + @ApiProperty({ description: '状态' }) + @IsOptional() + @IsInt() + status?: number; + + @ApiProperty({ description: '备注' }) + @IsOptional() + @IsString() + remark?: string; +} + +export class DictTypeQueryDto extends PagerDto { + @ApiProperty({ description: '字典类型名称' }) + @IsString() + @IsOptional() + name: string; + + @ApiProperty({ description: '字典类型code' }) + @IsString() + @IsOptional() + code: string; + + @ApiProperty({ description: '是否用于前端store缓存' }) + @IsOptional() + @IsBoolean() + withItems: boolean; + + @ApiProperty({ description: '需要前端store缓存的code' }) + @IsArray() + @IsOptional() + storeCodes: string[]; +} diff --git a/src/modules/system/dict-type/dict-type.entity.ts b/src/modules/system/dict-type/dict-type.entity.ts new file mode 100644 index 0000000..d9e46ad --- /dev/null +++ b/src/modules/system/dict-type/dict-type.entity.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, OneToMany, Relation } from 'typeorm'; + +import { CompleteEntity } from '~/common/entity/common.entity'; +import { DictItemEntity } from '../dict-item/dict-item.entity'; + +@Entity({ name: 'sys_dict_type' }) +export class DictTypeEntity extends CompleteEntity { + @Column({ type: 'varchar', length: 50 }) + @ApiProperty({ description: '字典名称' }) + name: string; + + @Column({ type: 'varchar', length: 50, unique: true }) + @ApiProperty({ description: '字典类型' }) + code: string; + + @Column({ type: 'tinyint', default: 1 }) + @ApiProperty({ description: ' 状态' }) + status: number; + + @Column({ type: 'varchar', nullable: true }) + @ApiProperty({ description: '备注' }) + remark: string; + + @OneToMany(() => DictItemEntity, dictItem => dictItem.type, { + cascade: true + }) + dictItems: Relation; +} diff --git a/src/modules/system/dict-type/dict-type.module.ts b/src/modules/system/dict-type/dict-type.module.ts new file mode 100644 index 0000000..626bb5e --- /dev/null +++ b/src/modules/system/dict-type/dict-type.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { DictTypeController } from './dict-type.controller'; +import { DictTypeEntity } from './dict-type.entity'; +import { DictTypeService } from './dict-type.service'; + +const services = [DictTypeService]; + +@Module({ + imports: [TypeOrmModule.forFeature([DictTypeEntity])], + controllers: [DictTypeController], + providers: [...services], + exports: [TypeOrmModule, ...services] +}) +export class DictTypeModule {} diff --git a/src/modules/system/dict-type/dict-type.service.ts b/src/modules/system/dict-type/dict-type.service.ts new file mode 100644 index 0000000..dd422dd --- /dev/null +++ b/src/modules/system/dict-type/dict-type.service.ts @@ -0,0 +1,94 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { In, Like, Repository } from 'typeorm'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { paginate } from '~/helper/paginate'; +import { Pagination } from '~/helper/paginate/pagination'; +import { DictTypeEntity } from '~/modules/system/dict-type/dict-type.entity'; + +import { DictTypeDto, DictTypeQueryDto } from './dict-type.dto'; +import { DictTypeStatusEnum } from '~/constants/enum'; + +@Injectable() +export class DictTypeService { + constructor( + @InjectRepository(DictTypeEntity) + private dictTypeRepository: Repository + ) {} + + /** + * 罗列所有配置 + */ + async page({ + page, + pageSize, + name, + code + }: DictTypeQueryDto): Promise> { + const queryBuilder = this.dictTypeRepository.createQueryBuilder('dict_type').where({ + ...(name && { name: Like(`%${name}%`) }), + ...(code && { code: Like(`%${code}%`) }) + }); + + return paginate(queryBuilder, { page, pageSize }); + } + + /** 一次性获取所有的字典类型 */ + async getAll({ withItems, storeCodes }: DictTypeQueryDto) { + const sqb = this.dictTypeRepository + .createQueryBuilder('dict_type') + .addSelect(['dict_type.name', 'dict_type.code', 'dict_type.status']); + if (withItems) { + sqb + .leftJoin('dict_type.dictItems', 'dictItems') + .addSelect(['dictItems.id', 'dictItems.value', 'dictItems.label', 'dictItems.status']); + } + sqb.where('dict_type.status =:status', { status: DictTypeStatusEnum.ENABLE }); + withItems && sqb.andWhere('dictItems.status =:status', { status: DictTypeStatusEnum.ENABLE }); + storeCodes && sqb.andWhere({ code: In(storeCodes) }); + return sqb.getMany(); + } + + /** + * 获取参数总数 + */ + async countConfigList(): Promise { + return this.dictTypeRepository.count(); + } + + /** + * 新增 + */ + async create(dto: DictTypeDto): Promise { + await this.dictTypeRepository.insert(dto); + } + + /** + * 更新 + */ + async update(id: number, dto: Partial): Promise { + await this.dictTypeRepository.update(id, dto); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + await this.dictTypeRepository.delete(id); + } + + /** + * 查询单个 + */ + async findOne(id: number): Promise { + return this.dictTypeRepository.findOneBy({ id }); + } + + async isExistKey(name: string): Promise { + const result = await this.dictTypeRepository.findOneBy({ name }); + if (result) throw new BusinessException(ErrorEnum.DICT_NAME_EXISTS); + } +} diff --git a/src/modules/system/log/dto/log.dto.ts b/src/modules/system/log/dto/log.dto.ts new file mode 100644 index 0000000..3b34900 --- /dev/null +++ b/src/modules/system/log/dto/log.dto.ts @@ -0,0 +1,65 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsArray, IsOptional, IsString } from 'class-validator'; + +import { PagerDto } from '~/common/dto/pager.dto'; +import { formatToDate } from '~/utils'; + +export class LoginLogQueryDto extends PagerDto { + @ApiProperty({ description: '用户名' }) + @IsString() + @IsOptional() + username: string; + + @ApiProperty({ description: '登录IP' }) + @IsOptional() + @IsString() + ip?: string; + + @ApiProperty({ description: '登录地点' }) + @IsOptional() + @IsString() + address?: string; + + @ApiProperty({ description: '登录时间' }) + @IsOptional() + @IsArray() + @Transform(params => { + // 开始和结束时间用的是一天的开始和一天的结束的时分秒 + const [start, end] = params.value; + return [start ? `${formatToDate(start)} 00:00:00` : null, end ? `${formatToDate(end)} 23:59:59` : null]; + }) + time?: string[]; +} + +export class TaskLogQueryDto extends PagerDto { + @ApiProperty({ description: '用户名' }) + @IsOptional() + @IsString() + username: string; + + @ApiProperty({ description: '登录IP' }) + @IsString() + @IsOptional() + ip?: string; + + @ApiProperty({ description: '登录时间' }) + @IsOptional() + time?: string[]; +} + +export class CaptchaLogQueryDto extends PagerDto { + @ApiProperty({ description: '用户名' }) + @IsOptional() + @IsString() + username: string; + + @ApiProperty({ description: '验证码' }) + @IsString() + @IsOptional() + code?: string; + + @ApiProperty({ description: '发送时间' }) + @IsOptional() + time?: string[]; +} diff --git a/src/modules/system/log/entities/captcha-log.entity.ts b/src/modules/system/log/entities/captcha-log.entity.ts new file mode 100644 index 0000000..a00bb5c --- /dev/null +++ b/src/modules/system/log/entities/captcha-log.entity.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Column, Entity } from 'typeorm'; + +import { CommonEntity } from '~/common/entity/common.entity'; + +@Entity({ name: 'sys_captcha_log' }) +export class CaptchaLogEntity extends CommonEntity { + @Column({ name: 'user_id', nullable: true }) + @ApiProperty({ description: '用户ID' }) + userId: number; + + @Column({ nullable: true }) + @ApiProperty({ description: '账号' }) + account: string; + + @Column({ nullable: true }) + @ApiProperty({ description: '验证码' }) + code: string; + + @Column({ nullable: true }) + @ApiProperty({ description: '验证码提供方' }) + provider: 'sms' | 'email'; +} diff --git a/src/modules/system/log/entities/index.ts b/src/modules/system/log/entities/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/system/log/entities/login-log.entity.ts b/src/modules/system/log/entities/login-log.entity.ts new file mode 100644 index 0000000..25d5826 --- /dev/null +++ b/src/modules/system/log/entities/login-log.entity.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinColumn, ManyToOne, Relation } from 'typeorm'; + +import { CommonEntity } from '~/common/entity/common.entity'; + +import { UserEntity } from '../../../user/user.entity'; + +@Entity({ name: 'sys_login_log' }) +export class LoginLogEntity extends CommonEntity { + @Column({ nullable: true }) + @ApiProperty({ description: 'IP' }) + ip: string; + + @Column({ nullable: true }) + @ApiProperty({ description: '地址' }) + address: string; + + @Column({ nullable: true }) + @ApiProperty({ description: '登录方式' }) + provider: string; + + @Column({ length: 500, nullable: true }) + @ApiProperty({ description: '浏览器ua' }) + ua: string; + + @ManyToOne(() => UserEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: Relation; +} diff --git a/src/modules/system/log/entities/task-log.entity.ts b/src/modules/system/log/entities/task-log.entity.ts new file mode 100644 index 0000000..9d7325f --- /dev/null +++ b/src/modules/system/log/entities/task-log.entity.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinColumn, ManyToOne, Relation } from 'typeorm'; + +import { CommonEntity } from '~/common/entity/common.entity'; + +import { TaskEntity } from '../../task/task.entity'; + +@Entity({ name: 'sys_task_log' }) +export class TaskLogEntity extends CommonEntity { + @Column({ type: 'tinyint', default: 0 }) + @ApiProperty({ description: '任务状态:0失败,1成功' }) + status: number; + + @Column({ type: 'text', nullable: true }) + @ApiProperty({ description: '任务日志信息' }) + detail: string; + + @Column({ type: 'int', nullable: true, name: 'consume_time', default: 0 }) + @ApiProperty({ description: '任务耗时' }) + consumeTime: number; + + @ManyToOne(() => TaskEntity) + @JoinColumn({ name: 'task_id' }) + task: Relation; +} diff --git a/src/modules/system/log/log.controller.ts b/src/modules/system/log/log.controller.ts new file mode 100644 index 0000000..b98c08d --- /dev/null +++ b/src/modules/system/log/log.controller.ts @@ -0,0 +1,56 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { Pagination } from '~/helper/paginate/pagination'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; + +import { CaptchaLogQueryDto, LoginLogQueryDto, TaskLogQueryDto } from './dto/log.dto'; +import { CaptchaLogEntity } from './entities/captcha-log.entity'; +import { TaskLogEntity } from './entities/task-log.entity'; +import { LoginLogInfo } from './models/log.model'; +import { CaptchaLogService } from './services/captcha-log.service'; +import { LoginLogService } from './services/login-log.service'; +import { TaskLogService } from './services/task-log.service'; + +export const permissions = definePermission('system:log', { + TaskList: 'task:list', + LogList: 'login:list', + CaptchaList: 'captcha:list' +} as const); + +@ApiSecurityAuth() +@ApiTags('System - 日志模块') +@Controller('log') +export class LogController { + constructor( + private loginLogService: LoginLogService, + private taskService: TaskLogService, + private captchaLogService: CaptchaLogService + ) {} + + @Get('login/list') + @ApiOperation({ summary: '查询登录日志列表' }) + @ApiResult({ type: [LoginLogInfo], isPage: true }) + @Perm(permissions.TaskList) + async loginLogPage(@Query() dto: LoginLogQueryDto): Promise> { + return this.loginLogService.list(dto); + } + + @Get('task/list') + @ApiOperation({ summary: '查询任务日志列表' }) + @ApiResult({ type: [TaskLogEntity], isPage: true }) + @Perm(permissions.LogList) + async taskList(@Query() dto: TaskLogQueryDto) { + return this.taskService.list(dto); + } + + @Get('captcha/list') + @ApiOperation({ summary: '查询验证码日志列表' }) + @ApiResult({ type: [CaptchaLogEntity], isPage: true }) + @Perm(permissions.CaptchaList) + async captchaList(@Query() dto: CaptchaLogQueryDto): Promise> { + return this.captchaLogService.paginate(dto); + } +} diff --git a/src/modules/system/log/log.module.ts b/src/modules/system/log/log.module.ts new file mode 100644 index 0000000..b39d2ea --- /dev/null +++ b/src/modules/system/log/log.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { UserModule } from '../../user/user.module'; + +import { CaptchaLogEntity } from './entities/captcha-log.entity'; +import { LoginLogEntity } from './entities/login-log.entity'; +import { TaskLogEntity } from './entities/task-log.entity'; +import { LogController } from './log.controller'; +import { CaptchaLogService } from './services/captcha-log.service'; +import { LoginLogService } from './services/login-log.service'; +import { TaskLogService } from './services/task-log.service'; + +const providers = [LoginLogService, TaskLogService, CaptchaLogService]; + +@Module({ + imports: [ + TypeOrmModule.forFeature([LoginLogEntity, CaptchaLogEntity, TaskLogEntity]), + UserModule + ], + controllers: [LogController], + providers: [...providers], + exports: [TypeOrmModule, ...providers] +}) +export class LogModule {} diff --git a/src/modules/system/log/models/log.model.ts b/src/modules/system/log/models/log.model.ts new file mode 100644 index 0000000..580f227 --- /dev/null +++ b/src/modules/system/log/models/log.model.ts @@ -0,0 +1,47 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class LoginLogInfo { + @ApiProperty({ description: '日志编号' }) + id: number; + + @ApiProperty({ description: '登录ip', example: '1.1.1.1' }) + ip: string; + + @ApiProperty({ description: '登录地址' }) + address: string; + + @ApiProperty({ description: '系统', example: 'Windows 10' }) + os: string; + + @ApiProperty({ description: '浏览器', example: 'Chrome' }) + browser: string; + + @ApiProperty({ description: '登录用户名', example: 'admin' }) + username: string; + + @ApiProperty({ description: '登录时间', example: '2023-12-22 16:46:20.333843' }) + time: string; +} + +export class TaskLogInfo { + @ApiProperty({ description: '日志编号' }) + id: number; + + @ApiProperty({ description: '任务编号' }) + taskId: number; + + @ApiProperty({ description: '任务名称' }) + name: string; + + @ApiProperty({ description: '创建时间' }) + createdAt: string; + + @ApiProperty({ description: '耗时' }) + consumeTime: number; + + @ApiProperty({ description: '执行信息' }) + detail: string; + + @ApiProperty({ description: '任务执行状态' }) + status: number; +} diff --git a/src/modules/system/log/services/captcha-log.service.ts b/src/modules/system/log/services/captcha-log.service.ts new file mode 100644 index 0000000..970a7d8 --- /dev/null +++ b/src/modules/system/log/services/captcha-log.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { LessThan, Repository } from 'typeorm'; + +import { paginate } from '~/helper/paginate'; + +import { CaptchaLogQueryDto } from '../dto/log.dto'; +import { CaptchaLogEntity } from '../entities/captcha-log.entity'; + +@Injectable() +export class CaptchaLogService { + constructor( + @InjectRepository(CaptchaLogEntity) + private captchaLogRepository: Repository + ) {} + + async create( + account: string, + code: string, + provider: 'sms' | 'email', + uid?: number + ): Promise { + await this.captchaLogRepository.save({ + account, + code, + provider, + userId: uid + }); + } + + async paginate({ page, pageSize }: CaptchaLogQueryDto) { + const queryBuilder = await this.captchaLogRepository + .createQueryBuilder('captcha_log') + .orderBy('captcha_log.id', 'DESC'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + async clearLog(): Promise { + await this.captchaLogRepository.clear(); + } + + async clearLogBeforeTime(time: Date): Promise { + await this.captchaLogRepository.delete({ createdAt: LessThan(time) }); + } +} diff --git a/src/modules/system/log/services/login-log.service.ts b/src/modules/system/log/services/login-log.service.ts new file mode 100644 index 0000000..6f03578 --- /dev/null +++ b/src/modules/system/log/services/login-log.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Between, LessThan, Like, Repository } from 'typeorm'; + +import UAParser from 'ua-parser-js'; + +import { paginateRaw } from '~/helper/paginate'; + +import { getIpAddress } from '~/utils/ip.util'; + +import { LoginLogQueryDto } from '../dto/log.dto'; +import { LoginLogEntity } from '../entities/login-log.entity'; +import { LoginLogInfo } from '../models/log.model'; + +async function parseLoginLog(e: any, parser: UAParser): Promise { + const uaResult = parser.setUA(e.login_log_ua).getResult(); + + return { + id: e.login_log_id, + ip: e.login_log_ip, + address: e.login_log_address, + os: `${`${uaResult.os.name ?? ''} `}${uaResult.os.version}`, + browser: `${`${uaResult.browser.name ?? ''} `}${uaResult.browser.version}`, + username: e.user_username, + time: e.login_log_created_at + }; +} + +@Injectable() +export class LoginLogService { + constructor( + @InjectRepository(LoginLogEntity) + private loginLogRepository: Repository + ) {} + + async create(uid: number, ip: string, ua: string): Promise { + try { + const address = await getIpAddress(ip); + + await this.loginLogRepository.save({ + ip, + ua, + address, + user: { id: uid } + }); + } catch (e) { + console.error(e); + } + } + + async list({ page, pageSize, username, ip, address, time }: LoginLogQueryDto) { + const queryBuilder = await this.loginLogRepository + .createQueryBuilder('login_log') + .innerJoinAndSelect('login_log.user', 'user') + .where({ + ...(ip && { ip: Like(`%${ip}%`) }), + ...(address && { address: Like(`%${address}%`) }), + ...(time && { createdAt: Between(time[0], time[1]) }), + ...(username && { + user: { + username: Like(`%${username}%`) + } + }) + }) + .orderBy('login_log.created_at', 'DESC'); + + const { items, ...rest } = await paginateRaw(queryBuilder, { + page, + pageSize + }); + + const parser = new UAParser(); + const loginLogInfos = await Promise.all(items.map(item => parseLoginLog(item, parser))); + + return { + items: loginLogInfos, + ...rest + }; + } + + async clearLog(): Promise { + await this.loginLogRepository.clear(); + } + + async clearLogBeforeTime(time: Date): Promise { + await this.loginLogRepository.delete({ createdAt: LessThan(time) }); + } +} diff --git a/src/modules/system/log/services/task-log.service.ts b/src/modules/system/log/services/task-log.service.ts new file mode 100644 index 0000000..c5fd706 --- /dev/null +++ b/src/modules/system/log/services/task-log.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { LessThan, Repository } from 'typeorm'; + +import { paginate } from '~/helper/paginate'; + +import { TaskLogQueryDto } from '../dto/log.dto'; +import { TaskLogEntity } from '../entities/task-log.entity'; + +@Injectable() +export class TaskLogService { + constructor( + @InjectRepository(TaskLogEntity) + private taskLogRepository: Repository + ) {} + + async create(tid: number, status: number, time?: number, err?: string): Promise { + const result = await this.taskLogRepository.save({ + status, + detail: err, + time, + task: { id: tid } + }); + return result.id; + } + + async list({ page, pageSize }: TaskLogQueryDto) { + const queryBuilder = await this.taskLogRepository + .createQueryBuilder('task_log') + .leftJoinAndSelect('task_log.task', 'task') + .orderBy('task_log.id', 'DESC'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + async clearLog(): Promise { + await this.taskLogRepository.clear(); + } + + async clearLogBeforeTime(time: Date): Promise { + await this.taskLogRepository.delete({ createdAt: LessThan(time) }); + } +} diff --git a/src/modules/system/menu/menu.controller.ts b/src/modules/system/menu/menu.controller.ts new file mode 100644 index 0000000..19cc977 --- /dev/null +++ b/src/modules/system/menu/menu.controller.ts @@ -0,0 +1,109 @@ +import { + BadRequestException, + Body, + Controller, + Delete, + Get, + Post, + Put, + Query +} from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { flattenDeep } from 'lodash'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { + Perm, + definePermission, + getDefinePermissions +} from '~/modules/auth/decorators/permission.decorator'; + +import { MenuDto, MenuQueryDto, MenuUpdateDto } from './menu.dto'; +import { MenuItemInfo } from './menu.model'; +import { MenuService } from './menu.service'; +import { IsMobile } from '~/common/decorators/http.decorator'; +import { ResourceDeviceEnum } from '~/constants/enum'; + +export const permissions = definePermission('system:menu', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('System - 菜单权限模块') +@ApiSecurityAuth() +@Controller('menus') +export class MenuController { + constructor(private menuService: MenuService) {} + + @Get() + @ApiOperation({ summary: '获取所有菜单列表' }) + @ApiResult({ type: [MenuItemInfo] }) + @Perm(permissions.LIST) + async list(@Query() dto: MenuQueryDto) { + return this.menuService.list({ + ...dto + }); + } + + @Get(':id') + @ApiOperation({ summary: '获取菜单或权限信息' }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.menuService.getMenuItemAndParentInfo(id); + } + + @Post() + @ApiOperation({ summary: '新增菜单或权限' }) + @Perm(permissions.CREATE) + async create(@Body() dto: MenuDto): Promise { + // check + await this.menuService.check(dto); + if (!dto.parentId) dto.parentId = null; + + await this.menuService.create(dto); + if (dto.type === 2) { + // 如果是权限发生更改,则刷新所有在线用户的权限 + await this.menuService.refreshOnlineUserPerms(); + } + } + + @Put(':id') + @ApiOperation({ summary: '更新菜单或权限' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: MenuUpdateDto): Promise { + // check + await this.menuService.check(dto); + if (dto.parentId === -1 || !dto.parentId) dto.parentId = null; + + await this.menuService.update(id, dto); + if (dto.type === 2) { + // 如果是权限发生更改,则刷新所有在线用户的权限 + await this.menuService.refreshOnlineUserPerms(); + } + } + + @Delete(':id') + @ApiOperation({ summary: '删除菜单或权限' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + if (await this.menuService.checkRoleByMenuId(id)) + throw new BadRequestException('该菜单存在关联角色,无法删除'); + + // 如果有子目录,一并删除 + const childMenus = await this.menuService.findChildMenus(id); + await this.menuService.deleteMenuItem(flattenDeep([id, childMenus])); + // 刷新在线用户权限 + await this.menuService.refreshOnlineUserPerms(); + } + + @Get('permissions') + @ApiOperation({ summary: '获取后端定义的所有权限集' }) + async getPermissions(): Promise { + return getDefinePermissions(); + } +} diff --git a/src/modules/system/menu/menu.dto.ts b/src/modules/system/menu/menu.dto.ts new file mode 100644 index 0000000..76439b3 --- /dev/null +++ b/src/modules/system/menu/menu.dto.ts @@ -0,0 +1,101 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { + IsBoolean, + IsIn, + IsInt, + IsNumber, + IsOptional, + IsString, + Min, + MinLength, + ValidateIf +} from 'class-validator'; + +export class MenuDto { + @ApiProperty({ description: '菜单类型' }) + @IsIn([0, 1, 2]) + type: number; + + @ApiProperty({ description: '客户端设备类型' }) + @IsOptional() + @IsIn([0, 1]) + device: number; + + @ApiProperty({ description: '父级菜单' }) + @IsOptional() + parentId: number; + + @ApiProperty({ description: '菜单或权限名称' }) + @IsString() + @MinLength(2) + name: string; + + @ApiProperty({ description: '排序' }) + @IsInt() + @Min(0) + orderNo: number; + + @ApiProperty({ description: '前端路由地址' }) + // @Matches(/^[/]$/) + @ValidateIf(o => o.type !== 2) + path: string; + + @ApiProperty({ description: '是否外链', default: false }) + @ValidateIf(o => o.type !== 2) + @IsBoolean() + isExt: boolean; + + @ApiProperty({ description: '外链打开方式', default: 1 }) + @ValidateIf((o: MenuDto) => o.isExt) + @IsIn([1, 2]) + extOpenMode: number; + + @ApiProperty({ description: '菜单是否显示', default: 1 }) + @ValidateIf((o: MenuDto) => o.type !== 2) + @IsIn([0, 1]) + show: number; + + @ApiProperty({ description: '设置当前路由高亮的菜单项,一般用于详情页' }) + @ValidateIf((o: MenuDto) => o.type !== 2 && o.show === 0) + @IsString() + @IsOptional() + activeMenu?: string; + + @ApiProperty({ description: '是否开启页面缓存', default: 1 }) + @ValidateIf((o: MenuDto) => o.type === 1) + @IsIn([0, 1]) + keepAlive: number; + + @ApiProperty({ description: '状态', default: 1 }) + @IsIn([0, 1]) + status: number; + + @ApiProperty({ description: '菜单图标' }) + @IsOptional() + @ValidateIf((o: MenuDto) => o.type !== 2) + @IsString() + icon?: string; + + @ApiProperty({ description: '对应权限' }) + @ValidateIf((o: MenuDto) => o.type === 2) + @IsString() + @IsOptional() + permission: string; + + @ApiProperty({ description: '菜单路由路径或外链' }) + @ValidateIf((o: MenuDto) => o.type !== 2) + @IsString() + @IsOptional() + component?: string; +} + +export class MenuUpdateDto extends PartialType(MenuDto) {} + +export class MenuQueryDto extends PartialType(MenuDto) { + + @ApiProperty({ description: 'App端的菜单权限' }) + @IsNumber() + @IsOptional() + isApp?: number; + +} diff --git a/src/modules/system/menu/menu.entity.ts b/src/modules/system/menu/menu.entity.ts new file mode 100644 index 0000000..48da32f --- /dev/null +++ b/src/modules/system/menu/menu.entity.ts @@ -0,0 +1,58 @@ +import { Column, Entity, ManyToMany, Relation } from 'typeorm'; + +import { CommonEntity } from '~/common/entity/common.entity'; + +import { RoleEntity } from '../role/role.entity'; + +@Entity({ name: 'sys_menu' }) +export class MenuEntity extends CommonEntity { + @Column({ name: 'parent_id', nullable: true, comment: '父级ID' }) + parentId: number; + + @Column() + name: string; + + @Column({ nullable: true, comment: '前端路径' }) + path: string; + + @Column({ nullable: true, comment: '权限' }) + permission: string; + + @Column({ type: 'tinyint', default: 0, comment: '类型:0-目录 1-菜单 2-权限' }) + type: number; + + @Column({ nullable: true, default: '', comment: '图标' }) + icon: string; + + @Column({ name: 'order_no', type: 'int', nullable: true, default: 0 }) + orderNo: number; + + @Column({ name: 'component', nullable: true, comment: '前端组件文件地址' }) + component: string; + + @Column({ name: 'is_ext', type: 'boolean', default: false }) + isExt: boolean; + + @Column({ name: 'ext_open_mode', type: 'tinyint', default: 1 }) + extOpenMode: number; + + @Column({ name: 'keep_alive', type: 'tinyint', default: 1 }) + keepAlive: number; + + @Column({ type: 'tinyint', default: 1 }) + show: number; + + @Column({ name: 'active_menu', nullable: true }) + activeMenu: string; + + @Column({ type: 'tinyint', default: 1 }) + status: number; + + @Column({ type: 'tinyint', default: 1,comment: '用户端类型:0-APP 1-PC' }) + device: number; + + @ManyToMany(() => RoleEntity, role => role.menus, { + onDelete: 'CASCADE' + }) + roles: Relation; +} diff --git a/src/modules/system/menu/menu.model.ts b/src/modules/system/menu/menu.model.ts new file mode 100644 index 0000000..27a3b4c --- /dev/null +++ b/src/modules/system/menu/menu.model.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { MenuEntity } from './menu.entity'; + +export class MenuItemInfo extends MenuEntity { + @ApiProperty({ type: [MenuItemInfo] }) + children: MenuItemInfo[]; +} diff --git a/src/modules/system/menu/menu.module.ts b/src/modules/system/menu/menu.module.ts new file mode 100644 index 0000000..03aa7c2 --- /dev/null +++ b/src/modules/system/menu/menu.module.ts @@ -0,0 +1,20 @@ +import { Module, forwardRef } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { SseService } from '~/modules/sse/sse.service'; + +import { RoleModule } from '../role/role.module'; + +import { MenuController } from './menu.controller'; +import { MenuEntity } from './menu.entity'; +import { MenuService } from './menu.service'; + +const providers = [MenuService, SseService]; + +@Module({ + imports: [TypeOrmModule.forFeature([MenuEntity]), forwardRef(() => RoleModule)], + controllers: [MenuController], + providers: [...providers], + exports: [TypeOrmModule, ...providers] +}) +export class MenuModule {} diff --git a/src/modules/system/menu/menu.service.ts b/src/modules/system/menu/menu.service.ts new file mode 100644 index 0000000..ba8c723 --- /dev/null +++ b/src/modules/system/menu/menu.service.ts @@ -0,0 +1,262 @@ +import { InjectRedis } from '@liaoliaots/nestjs-redis'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import Redis from 'ioredis'; +import { concat, isEmpty, isNumber, uniq } from 'lodash'; + +import { In, IsNull, Like, Not, Repository } from 'typeorm'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { RedisKeys } from '~/constants/cache.constant'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { genAuthPermKey, genAuthTokenKey } from '~/helper/genRedisKey'; +import { SseService } from '~/modules/sse/sse.service'; +import { MenuEntity } from '~/modules/system/menu/menu.entity'; + +import { deleteEmptyChildren, generatorMenu, generatorRouters } from '~/utils'; + +import { RoleService } from '../role/role.service'; + +import { MenuDto, MenuQueryDto, MenuUpdateDto } from './menu.dto'; +import { ResourceDeviceEnum } from '~/constants/enum'; + +@Injectable() +export class MenuService { + constructor( + @InjectRedis() private redis: Redis, + @InjectRepository(MenuEntity) + private menuRepository: Repository, + private roleService: RoleService, + private sseService: SseService + ) {} + + /** + * 获取所有菜单以及权限 + */ + async list({ + name, + path, + permission, + component, + status, + isApp + }: MenuQueryDto): Promise { + const menus = await this.menuRepository.find({ + where: { + ...(name && { name: Like(`%${name}%`) }), + ...(path && { path: Like(`%${path}%`) }), + ...(permission && { permission: Like(`%${permission}%`) }), + ...(component && { component: Like(`%${component}%`) }), + ...(isNumber(status) ? { status } : null) + }, + order: { orderNo: 'ASC' } + }); + const menuList = generatorMenu(menus); + + if (!isEmpty(menuList)) { + deleteEmptyChildren(menuList); + return menuList; + } + // 如果生产树形结构为空,则返回原始菜单列表 + return menus; + } + + async create(menu: MenuDto): Promise { + const result = await this.menuRepository.save(menu); + this.sseService.noticeClientToUpdateMenusByMenuIds([result.id]); + } + + async update(id: number, menu: MenuUpdateDto): Promise { + await this.menuRepository.update(id, menu); + this.sseService.noticeClientToUpdateMenusByMenuIds([id]); + } + + /** + * 根据角色获取所有菜单 + */ + async getMenus(uid: number, deviceType: number): Promise { + const roleIds = await this.roleService.getRoleIdsByUser(uid); + let menus: MenuEntity[] = []; + + if (isEmpty(roleIds)) return generatorRouters([]); + + if (this.roleService.hasAdminRole(roleIds)) { + menus = await this.menuRepository.find({ + order: { orderNo: 'ASC' }, + where: { + ...(isNumber(deviceType) ? { device: deviceType } : null) + } + }); + } else { + menus = await this.menuRepository + .createQueryBuilder('menu') + .innerJoinAndSelect('menu.roles', 'role') + .where({ + ...(isNumber(deviceType) ? { device: deviceType } : null) + }) + .andWhere('role.id IN (:...roleIds)', { roleIds }) + .orderBy('menu.order_no', 'ASC') + + .getMany(); + } + + const menuList = generatorRouters(menus); + return menuList; + } + + /** + * 检查菜单创建规则是否符合 + */ + async check(dto: Partial): Promise { + if (dto.type === 2 && !dto.parentId) { + // 无法直接创建权限,必须有parent + throw new BusinessException(ErrorEnum.PERMISSION_REQUIRES_PARENT); + } + if (dto.type === 1 && dto.parentId) { + const parent = await this.getMenuItemInfo(dto.parentId); + if (isEmpty(parent)) throw new BusinessException(ErrorEnum.PARENT_MENU_NOT_FOUND); + + if (parent && parent.type === 1) { + // 当前新增为菜单但父节点也为菜单时为非法操作 + throw new BusinessException(ErrorEnum.ILLEGAL_OPERATION_DIRECTORY_PARENT); + } + } + } + + /** + * 查找当前菜单下的子菜单,目录以及菜单 + */ + async findChildMenus(mid: number): Promise { + const allMenus: any = []; + const menus = await this.menuRepository.findBy({ parentId: mid }); + // if (_.isEmpty(menus)) { + // return allMenus; + // } + // const childMenus: any = []; + for (const menu of menus) { + if (menu.type !== 2) { + // 子目录下是菜单或目录,继续往下级查找 + const c = await this.findChildMenus(menu.id); + allMenus.push(c); + } + allMenus.push(menu.id); + } + return allMenus; + } + + /** + * 获取某个菜单的信息 + * @param mid menu id + */ + async getMenuItemInfo(mid: number): Promise { + const menu = await this.menuRepository.findOneBy({ id: mid }); + return menu; + } + + /** + * 获取某个菜单以及关联的父菜单的信息 + */ + async getMenuItemAndParentInfo(mid: number) { + const menu = await this.menuRepository.findOneBy({ id: mid }); + let parentMenu: MenuEntity | undefined; + if (menu && menu.parentId) + parentMenu = await this.menuRepository.findOneBy({ id: menu.parentId }); + + return { menu, parentMenu }; + } + + /** + * 查找节点路由是否存在 + */ + async findRouterExist(path: string): Promise { + const menus = await this.menuRepository.findOneBy({ path }); + return !isEmpty(menus); + } + + /** + * 获取当前用户的所有权限 + */ + async getPermissions(uid: number): Promise { + const roleIds = await this.roleService.getRoleIdsByUser(uid); + let permission: any[] = []; + let result: any = null; + if (this.roleService.hasAdminRole(roleIds)) { + result = await this.menuRepository.findBy({ + permission: Not(IsNull()), + type: In([1, 2]) + }); + } else { + if (isEmpty(roleIds)) return permission; + + result = await this.menuRepository + .createQueryBuilder('menu') + .innerJoinAndSelect('menu.roles', 'role') + .andWhere('role.id IN (:...roleIds)', { roleIds }) + .andWhere('menu.type IN (1,2)') + .andWhere('menu.permission IS NOT NULL') + .getMany(); + } + if (!isEmpty(result)) { + result.forEach(e => { + if (e.permission) permission = concat(permission, e.permission.split(',')); + }); + permission = uniq(permission); + } + return permission; + } + + /** + * 删除多项菜单 + */ + async deleteMenuItem(mids: number[]): Promise { + await this.menuRepository.delete(mids); + } + + /** + * 刷新指定用户ID的权限 + */ + async refreshPerms(uid: number): Promise { + const perms = await this.getPermissions(uid); + const online = await this.redis.get(genAuthTokenKey(uid)); + if (online) { + // 判断是否在线 + await this.redis.set(genAuthPermKey(uid), JSON.stringify(perms)); + console.log('refreshPerms'); + + this.sseService.noticeClientToUpdateMenusByUserIds([uid]); + } + } + + /** + * 刷新所有在线用户的权限 + */ + async refreshOnlineUserPerms(): Promise { + const onlineUserIds: string[] = await this.redis.keys(genAuthTokenKey('*')); + if (onlineUserIds && onlineUserIds.length > 0) { + const promiseArr = onlineUserIds + .map(i => Number.parseInt(i.split(RedisKeys.AUTH_TOKEN_PREFIX)[1])) + .filter(i => i) + .map(async uid => { + const perms = await this.getPermissions(uid); + await this.redis.set(genAuthPermKey(uid), JSON.stringify(perms)); + return uid; + }); + const uids = await Promise.all(promiseArr); + console.log('refreshOnlineUserPerms'); + this.sseService.noticeClientToUpdateMenusByUserIds(uids); + } + } + + /** + * 根据菜单ID查找是否有关联角色 + */ + async checkRoleByMenuId(id: number): Promise { + return !!(await this.menuRepository.findOne({ + where: { + roles: { + id + } + } + })); + } +} diff --git a/src/modules/system/online/online.controller.ts b/src/modules/system/online/online.controller.ts new file mode 100644 index 0000000..22c96f8 --- /dev/null +++ b/src/modules/system/online/online.controller.ts @@ -0,0 +1,45 @@ +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { ApiExtraModels, ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; + +import { AuthUser } from '~/modules/auth/decorators/auth-user.decorator'; + +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; + +import { KickDto } from './online.dto'; +import { OnlineUserInfo } from './online.model'; +import { OnlineService } from './online.service'; + +export const permissions = definePermission('system:online', { + LIST: 'list', + KICK: 'kick' +} as const); + +@ApiTags('System - 在线用户模块') +@ApiSecurityAuth() +@ApiExtraModels(OnlineUserInfo) +@Controller('online') +export class OnlineController { + constructor(private onlineService: OnlineService) {} + + @Get('list') + @ApiOperation({ summary: '查询当前在线用户' }) + @ApiResult({ type: [OnlineUserInfo] }) + @Perm(permissions.LIST) + async list(@AuthUser() user: IAuthUser): Promise { + return this.onlineService.listOnlineUser(user.uid); + } + + @Post('kick') + @ApiOperation({ summary: '下线指定在线用户' }) + @Perm(permissions.KICK) + async kick(@Body() dto: KickDto, @AuthUser() user: IAuthUser): Promise { + if (dto.id === user.uid) throw new BusinessException(ErrorEnum.NOT_ALLOWED_TO_LOGOUT_USER); + + await this.onlineService.kickUser(dto.id, user.uid); + } +} diff --git a/src/modules/system/online/online.dto.ts b/src/modules/system/online/online.dto.ts new file mode 100644 index 0000000..736a2c8 --- /dev/null +++ b/src/modules/system/online/online.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt } from 'class-validator'; + +export class KickDto { + @ApiProperty({ description: '需要下线的角色ID' }) + @IsInt() + id: number; +} diff --git a/src/modules/system/online/online.model.ts b/src/modules/system/online/online.model.ts new file mode 100644 index 0000000..9c140d3 --- /dev/null +++ b/src/modules/system/online/online.model.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class OnlineUserInfo { + @ApiProperty({ description: '最近的一条登录日志ID' }) + id: number; + + @ApiProperty({ description: '登录IP' }) + ip: string; + + @ApiProperty({ description: '登录地点' }) + address: string; + + @ApiProperty({ description: '用户名' }) + username: string; + + @ApiProperty({ description: '是否当前' }) + isCurrent: boolean; + + @ApiProperty({ description: '系统' }) + os: string; + + @ApiProperty({ description: '浏览器' }) + browser: string; + + @ApiProperty({ description: '是否禁用' }) + disable: boolean; +} diff --git a/src/modules/system/online/online.module.ts b/src/modules/system/online/online.module.ts new file mode 100644 index 0000000..85ec082 --- /dev/null +++ b/src/modules/system/online/online.module.ts @@ -0,0 +1,27 @@ +import { Module, forwardRef } from '@nestjs/common'; + +import { AuthModule } from '~/modules/auth/auth.module'; +import { SocketModule } from '~/socket/socket.module'; + +import { UserModule } from '../../user/user.module'; +import { RoleModule } from '../role/role.module'; +import { SystemModule } from '../system.module'; + +import { OnlineController } from './online.controller'; +import { OnlineService } from './online.service'; + +const providers = [OnlineService]; + +@Module({ + imports: [ + forwardRef(() => SystemModule), + forwardRef(() => SocketModule), + AuthModule, + UserModule, + RoleModule + ], + controllers: [OnlineController], + providers, + exports: [...providers] +}) +export class OnlineModule {} diff --git a/src/modules/system/online/online.service.ts b/src/modules/system/online/online.service.ts new file mode 100644 index 0000000..39ba7c2 --- /dev/null +++ b/src/modules/system/online/online.service.ts @@ -0,0 +1,127 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { InjectEntityManager } from '@nestjs/typeorm'; + +import { RemoteSocket } from 'socket.io'; +import { EntityManager } from 'typeorm'; + +import { UAParser } from 'ua-parser-js'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; + +import { BusinessEvents } from '~/socket/business-event.constant'; +import { AdminEventsGateway } from '~/socket/events/admin.gateway'; + +import { UserService } from '../../user/user.service'; + +import { OnlineUserInfo } from './online.model'; + +@Injectable() +export class OnlineService { + constructor( + @InjectEntityManager() private readonly entityManager: EntityManager, + private readonly userService: UserService, + private readonly adminEventsGateWay: AdminEventsGateway, + private readonly jwtService: JwtService + ) {} + + /** + * 罗列在线用户列表 + */ + async listOnlineUser(currentUid: number): Promise { + const onlineSockets = await this.getOnlineSockets(); + if (!onlineSockets || onlineSockets.length <= 0) return []; + + const onlineIds = onlineSockets.map(socket => { + const token = socket.handshake.query?.token as string; + return this.jwtService.verify(token).uid; + }); + return this.findLastLoginInfoList(onlineIds, currentUid); + } + + /** + * 下线当前用户 + */ + async kickUser(uid: number, currentUid: number): Promise { + const rootUserId = await this.userService.findRootUserId(); + const currentUserInfo = await this.userService.getAccountInfo(currentUid); + if (uid === rootUserId) throw new BusinessException(ErrorEnum.NOT_ALLOWED_TO_LOGOUT_USER); + + // reset redis keys + await this.userService.forbidden(uid); + // socket emit + const socket = await this.findSocketIdByUid(uid); + if (socket) { + // socket emit event + this.adminEventsGateWay.server + .to(socket.id) + .emit(BusinessEvents.USER_KICK, { operater: currentUserInfo.username }); + // close socket + socket.disconnect(); + } + } + + /** + * 根据用户id列表查找最近登录信息和用户信息 + */ + async findLastLoginInfoList(ids: number[], currentUid: number): Promise { + const rootUserId = await this.userService.findRootUserId(); + const result = await this.entityManager.query( + ` + SELECT sys_login_log.created_at, sys_login_log.ip, sys_login_log.address, sys_login_log.ua, sys_user.id, sys_user.username, sys_user.nick_name + FROM sys_login_log + INNER JOIN sys_user ON sys_login_log.user_id = sys_user.id + WHERE sys_login_log.created_at IN (SELECT MAX(created_at) as createdAt FROM sys_login_log GROUP BY user_id) + AND sys_user.id IN (?) + `, + [ids] + ); + if (result) { + const parser = new UAParser(); + return result.map(e => { + const u = parser.setUA(e.ua).getResult(); + return { + id: e.id, + ip: e.ip, + address: e.address, + username: `${e.nick_name}(${e.username})`, + isCurrent: currentUid === e.id, + time: e.created_at, + os: `${u.os.name} ${u.os.version}`, + browser: `${u.browser.name} ${u.browser.version}`, + disable: currentUid === e.id || e.id === rootUserId + }; + }); + } + return []; + } + + /** + * 根据uid查找socketid + */ + async findSocketIdByUid(uid: number): Promise> { + const onlineSockets = await this.getOnlineSockets(); + const socket = onlineSockets.find(socket => { + const token = socket.handshake.query?.token as string; + const tokenUid = this.jwtService.verify(token).uid; + return tokenUid === uid; + }); + return socket; + } + + async filterSocketIdByUidArr(uids: number[]): Promise[]> { + const onlineSockets = await this.getOnlineSockets(); + const sockets = onlineSockets.filter(socket => { + const token = socket.handshake.query?.token as string; + const tokenUid = this.jwtService.verify(token).uid; + return uids.includes(tokenUid); + }); + return sockets; + } + + async getOnlineSockets() { + const onlineSockets = await this.adminEventsGateWay.server.fetchSockets(); + return onlineSockets; + } +} diff --git a/src/modules/system/param-config/param-config.controller.ts b/src/modules/system/param-config/param-config.controller.ts new file mode 100644 index 0000000..3214d79 --- /dev/null +++ b/src/modules/system/param-config/param-config.controller.ts @@ -0,0 +1,70 @@ +import { Body, Controller, Delete, Get, Param, Post, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { Pagination } from '~/helper/paginate/pagination'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { ParamConfigEntity } from '~/modules/system/param-config/param-config.entity'; + +import { ParamConfigDto, ParamConfigQueryDto } from './param-config.dto'; +import { ParamConfigService } from './param-config.service'; + +export const permissions = definePermission('system:param-config', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('System - 参数配置模块') +@ApiSecurityAuth() +@Controller('param-config') +export class ParamConfigController { + constructor(private paramConfigService: ParamConfigService) {} + + @Get() + @ApiOperation({ summary: '获取参数配置列表' }) + @ApiResult({ type: [ParamConfigEntity], isPage: true }) + async list(@Query() dto: ParamConfigQueryDto): Promise> { + return this.paramConfigService.page(dto); + } + + @Post() + @ApiOperation({ summary: '新增参数配置' }) + @Perm(permissions.CREATE) + async create(@Body() dto: ParamConfigDto): Promise { + await this.paramConfigService.isExistKey(dto.key); + await this.paramConfigService.create(dto); + } + + @Get('key/:code') + @ApiOperation({ summary: '查询参数配置信息By key' }) + @ApiResult({ type: String }) + async code(@Param('code') code: string): Promise { + return this.paramConfigService.findValueByKey(code); + } + + @Get(':id') + @ApiOperation({ summary: '查询参数配置信息' }) + @ApiResult({ type: ParamConfigEntity }) + async info(@IdParam() id: number): Promise { + return this.paramConfigService.findOne(id); + } + + @Post(':id') + @ApiOperation({ summary: '更新参数配置' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: ParamConfigDto): Promise { + await this.paramConfigService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除指定的参数配置' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.paramConfigService.delete(id); + } +} diff --git a/src/modules/system/param-config/param-config.dto.ts b/src/modules/system/param-config/param-config.dto.ts new file mode 100644 index 0000000..921d5ed --- /dev/null +++ b/src/modules/system/param-config/param-config.dto.ts @@ -0,0 +1,31 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString, MinLength } from 'class-validator'; + +import { PagerDto } from '~/common/dto/pager.dto'; + +export class ParamConfigDto { + @ApiProperty({ description: '参数名称' }) + @IsString() + name: string; + + @ApiProperty({ description: '参数键名' }) + @IsString() + @MinLength(3) + key: string; + + @ApiProperty({ description: '参数值' }) + @IsString() + value: string; + + @ApiProperty({ description: '备注' }) + @IsOptional() + @IsString() + remark?: string; +} + +export class ParamConfigQueryDto extends PagerDto { + @ApiProperty({ description: '参数名称' }) + @IsString() + @IsOptional() + name: string; +} diff --git a/src/modules/system/param-config/param-config.entity.ts b/src/modules/system/param-config/param-config.entity.ts new file mode 100644 index 0000000..47153e7 --- /dev/null +++ b/src/modules/system/param-config/param-config.entity.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Column, Entity } from 'typeorm'; + +import { CommonEntity } from '~/common/entity/common.entity'; + +@Entity({ name: 'sys_config' }) +export class ParamConfigEntity extends CommonEntity { + @Column({ type: 'varchar', length: 50 }) + @ApiProperty({ description: '配置名' }) + name: string; + + @Column({ type: 'varchar', length: 50, unique: true }) + @ApiProperty({ description: '配置键名' }) + key: string; + + @Column({ type: 'varchar', nullable: true }) + @ApiProperty({ description: '配置值' }) + value: string; + + @Column({ type: 'varchar', nullable: true }) + @ApiProperty({ description: '配置描述' }) + remark: string; +} diff --git a/src/modules/system/param-config/param-config.module.ts b/src/modules/system/param-config/param-config.module.ts new file mode 100644 index 0000000..e7797b4 --- /dev/null +++ b/src/modules/system/param-config/param-config.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { ParamConfigController } from './param-config.controller'; +import { ParamConfigEntity } from './param-config.entity'; +import { ParamConfigService } from './param-config.service'; + +const services = [ParamConfigService]; + +@Module({ + imports: [TypeOrmModule.forFeature([ParamConfigEntity])], + controllers: [ParamConfigController], + providers: [...services], + exports: [TypeOrmModule, ...services] +}) +export class ParamConfigModule {} diff --git a/src/modules/system/param-config/param-config.service.ts b/src/modules/system/param-config/param-config.service.ts new file mode 100644 index 0000000..7f185ff --- /dev/null +++ b/src/modules/system/param-config/param-config.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { paginate } from '~/helper/paginate'; +import { Pagination } from '~/helper/paginate/pagination'; +import { ParamConfigEntity } from '~/modules/system/param-config/param-config.entity'; + +import { ParamConfigDto, ParamConfigQueryDto } from './param-config.dto'; + +@Injectable() +export class ParamConfigService { + constructor( + @InjectRepository(ParamConfigEntity) + private paramConfigRepository: Repository + ) {} + + /** + * 罗列所有配置 + */ + async page({ + page, + pageSize, + name + }: ParamConfigQueryDto): Promise> { + const queryBuilder = this.paramConfigRepository.createQueryBuilder('config'); + + if (name) { + queryBuilder.where('config.name LIKE :name', { + name: `%${name}%` + }); + } + + return paginate(queryBuilder, { page, pageSize }); + } + + /** + * 获取参数总数 + */ + async countConfigList(): Promise { + return this.paramConfigRepository.count(); + } + + /** + * 新增 + */ + async create(dto: ParamConfigDto): Promise { + await this.paramConfigRepository.insert(dto); + } + + /** + * 更新 + */ + async update(id: number, dto: Partial): Promise { + await this.paramConfigRepository.update(id, dto); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + await this.paramConfigRepository.delete(id); + } + + /** + * 查询单个 + */ + async findOne(id: number): Promise { + return this.paramConfigRepository.findOneBy({ id }); + } + + async isExistKey(key: string): Promise { + const result = await this.paramConfigRepository.findOneBy({ key }); + if (result) throw new BusinessException(ErrorEnum.PARAMETER_CONFIG_KEY_EXISTS); + } + + async findValueByKey(key: string): Promise { + const result = await this.paramConfigRepository.findOne({ + where: { key }, + select: ['value'] + }); + if (result) return result.value; + + return null; + } +} diff --git a/src/modules/system/role/role.controller.ts b/src/modules/system/role/role.controller.ts new file mode 100644 index 0000000..192ca13 --- /dev/null +++ b/src/modules/system/role/role.controller.ts @@ -0,0 +1,84 @@ +import { + BadRequestException, + Body, + Controller, + Delete, + Get, + Post, + Put, + Query +} from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { RoleEntity } from '~/modules/system/role/role.entity'; + +import { MenuService } from '../menu/menu.service'; + +import { RoleDto, RoleQueryDto, RoleUpdateDto } from './role.dto'; +import { RoleInfo } from './role.model'; +import { RoleService } from './role.service'; + +export const permissions = definePermission('system:role', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('System - 角色模块') +@ApiSecurityAuth() +@Controller('roles') +export class RoleController { + constructor( + private roleService: RoleService, + private menuService: MenuService + ) {} + + @Get() + @ApiOperation({ summary: '获取角色列表' }) + @ApiResult({ type: [RoleEntity], isPage: true }) + @Perm(permissions.LIST) + async list(@Query() dto: RoleQueryDto) { + return this.roleService.findAll(dto); + } + + @Get(':id') + @ApiOperation({ summary: '获取角色信息' }) + @ApiResult({ type: RoleInfo }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.roleService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增角色' }) + @Perm(permissions.CREATE) + async create(@Body() dto: RoleDto): Promise { + await this.roleService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新角色' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: RoleUpdateDto): Promise { + await this.roleService.update(id, dto); + await this.menuService.refreshOnlineUserPerms(); + } + + @Delete(':id') + @ApiOperation({ summary: '删除角色' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + if (await this.roleService.checkUserByRoleId(id)) + throw new BadRequestException('该角色存在关联用户,无法删除'); + + await this.roleService.delete(id); + await this.menuService.refreshOnlineUserPerms(); + } +} diff --git a/src/modules/system/role/role.dto.ts b/src/modules/system/role/role.dto.ts new file mode 100644 index 0000000..6fef513 --- /dev/null +++ b/src/modules/system/role/role.dto.ts @@ -0,0 +1,43 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { IsArray, IsIn, IsInt, IsOptional, IsString, Matches, MinLength } from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; + +export class RoleDto { + @ApiProperty({ description: '角色名称' }) + @IsString() + @MinLength(2, { message: '角色名称长度不能小于2' }) + name: string; + + @ApiProperty({ description: '角色值' }) + @IsString() + @Matches(/^[a-z0-9A-Z]+$/, { message: '角色值只能包含字母和数字' }) + @MinLength(2, { message: '角色值长度不能小于2' }) + value: string; + + @ApiProperty({ description: '角色备注' }) + @IsString() + @IsOptional() + remark?: string; + + @ApiProperty({ description: '状态' }) + @IsIn([0, 1]) + status: number; + + @ApiProperty({ description: '关联菜单、权限编号' }) + @IsOptional() + @IsArray() + menuIds?: number[]; +} + +export class RoleUpdateDto extends PartialType(RoleDto) {} +export class RoleQueryDto extends IntersectionType(PagerDto, PartialType(RoleDto)) { + @ApiProperty({ description: '状态', example: 0, required: false }) + @IsInt() + @IsOptional() + status?: number; + + @ApiProperty({ description: '用于下拉框选择', required: false }) + @IsInt() + @IsOptional() + useForSelect: number; +} diff --git a/src/modules/system/role/role.entity.ts b/src/modules/system/role/role.entity.ts new file mode 100644 index 0000000..58d2159 --- /dev/null +++ b/src/modules/system/role/role.entity.ts @@ -0,0 +1,43 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinTable, ManyToMany, Relation } from 'typeorm'; + +import { CommonEntity } from '~/common/entity/common.entity'; + +import { UserEntity } from '../../user/user.entity'; +import { MenuEntity } from '../menu/menu.entity'; + +@Entity({ name: 'sys_role' }) +export class RoleEntity extends CommonEntity { + @Column({ length: 50, unique: true }) + @ApiProperty({ description: '角色名' }) + name: string; + + @Column({ unique: true }) + @ApiProperty({ description: '角色标识' }) + value: string; + + @Column({ nullable: true }) + @ApiProperty({ description: '角色描述' }) + remark: string; + + @Column({ type: 'tinyint', nullable: true, default: 1 }) + @ApiProperty({ description: '状态:1启用,0禁用' }) + status: number; + + @Column({ nullable: true }) + @ApiProperty({ description: '是否默认用户' }) + default: boolean; + + @ApiHideProperty() + @ManyToMany(() => UserEntity, user => user.roles) + users: Relation; + + @ApiHideProperty() + @ManyToMany(() => MenuEntity, menu => menu.roles, {}) + @JoinTable({ + name: 'sys_role_menus', + joinColumn: { name: 'role_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'menu_id', referencedColumnName: 'id' } + }) + menus: Relation; +} diff --git a/src/modules/system/role/role.model.ts b/src/modules/system/role/role.model.ts new file mode 100644 index 0000000..285df41 --- /dev/null +++ b/src/modules/system/role/role.model.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { RoleEntity } from './role.entity'; + +export class RoleInfo extends RoleEntity { + @ApiProperty({ type: [Number] }) + menuIds: number[]; +} diff --git a/src/modules/system/role/role.module.ts b/src/modules/system/role/role.module.ts new file mode 100644 index 0000000..087fef6 --- /dev/null +++ b/src/modules/system/role/role.module.ts @@ -0,0 +1,20 @@ +import { Module, forwardRef } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { SseService } from '~/modules/sse/sse.service'; + +import { MenuModule } from '../menu/menu.module'; + +import { RoleController } from './role.controller'; +import { RoleEntity } from './role.entity'; +import { RoleService } from './role.service'; + +const providers = [RoleService, SseService]; + +@Module({ + imports: [TypeOrmModule.forFeature([RoleEntity]), forwardRef(() => MenuModule)], + controllers: [RoleController], + providers: [...providers], + exports: [TypeOrmModule, ...providers] +}) +export class RoleModule {} diff --git a/src/modules/system/role/role.service.ts b/src/modules/system/role/role.service.ts new file mode 100644 index 0000000..7b591ad --- /dev/null +++ b/src/modules/system/role/role.service.ts @@ -0,0 +1,160 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { isEmpty, isNumber } from 'lodash'; +import { EntityManager, In, Like, Repository } from 'typeorm'; + +import { PagerDto } from '~/common/dto/pager.dto'; +import { ROOT_ROLE_ID } from '~/constants/system.constant'; +import { paginate } from '~/helper/paginate'; +import { Pagination } from '~/helper/paginate/pagination'; +import { SseService } from '~/modules/sse/sse.service'; +import { MenuEntity } from '~/modules/system/menu/menu.entity'; +import { RoleEntity } from '~/modules/system/role/role.entity'; + +import { RoleDto, RoleQueryDto, RoleUpdateDto } from './role.dto'; + +@Injectable() +export class RoleService { + constructor( + @InjectRepository(RoleEntity) + private roleRepository: Repository, + @InjectRepository(MenuEntity) + private menuRepository: Repository, + @InjectEntityManager() private entityManager: EntityManager, + private sseService: SseService + ) {} + + /** + * 列举所有角色:除去超级管理员 + */ + async findAll({ + page, + pageSize, + name, + value, + status, + useForSelect + }: RoleQueryDto): Promise> { + const queryBuilder = this.roleRepository.createQueryBuilder('role').where({ + ...(name ? { name: Like(`%${name}%`) } : null), + ...(value ? { value: Like(`%${value}%`) } : null), + ...(isNumber(status) ? { status } : null) + }); + if(useForSelect){ + queryBuilder.andWhere('role.id != :id', { id: ROOT_ROLE_ID }) + } + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 根据角色获取角色信息 + */ + async info(id: number) { + const info = await this.roleRepository + .createQueryBuilder('role') + .where({ + id + }) + .getOne(); + + const menus = await this.menuRepository.find({ + where: { roles: { id } }, + select: ['id'] + }); + + return { ...info, menuIds: menus.map(m => m.id) }; + } + + async delete(id: number): Promise { + if (id === ROOT_ROLE_ID) throw new Error('不能删除超级管理员'); + await this.roleRepository.delete(id); + } + + /** + * 增加角色 + */ + async create({ menuIds, ...data }: RoleDto): Promise<{ roleId: number }> { + const role = await this.roleRepository.save({ + ...data, + menus: menuIds ? await this.menuRepository.findBy({ id: In(menuIds) }) : [] + }); + + return { roleId: role.id }; + } + + /** + * 更新角色信息 + */ + async update(id, { menuIds, ...data }: RoleUpdateDto): Promise { + await this.roleRepository.update(id, data); + + if (!isEmpty(menuIds)) { + // using transaction + await this.entityManager.transaction(async manager => { + const menus = await this.menuRepository.find({ + where: { id: In(menuIds) } + }); + + const role = await this.roleRepository.findOne({ where: { id } }); + role.menus = menus; + await manager.save(role); + }); + } + } + + /** + * 根据用户id查找角色信息 + */ + async getRoleIdsByUser(id: number): Promise { + const roles = await this.roleRepository.find({ + where: { + users: { id } + } + }); + + if (!isEmpty(roles)) return roles.map(r => r.id); + + return []; + } + + async getRoleValues(ids: number[]): Promise { + return ( + await this.roleRepository.findBy({ + id: In(ids) + }) + ).map(r => r.value); + } + + async isAdminRoleByUser(uid: number): Promise { + const roles = await this.roleRepository.find({ + where: { + users: { id: uid } + } + }); + + if (!isEmpty(roles)) { + return roles.some(r => r.id === ROOT_ROLE_ID); + } + return false; + } + + hasAdminRole(rids: number[]): boolean { + return rids.includes(ROOT_ROLE_ID); + } + + /** + * 根据角色ID查找是否有关联用户 + */ + async checkUserByRoleId(id: number): Promise { + return this.roleRepository.exist({ + where: { + users: { + roles: { id } + } + } + }); + } +} diff --git a/src/modules/system/serve/serve.controller.ts b/src/modules/system/serve/serve.controller.ts new file mode 100644 index 0000000..d931f4d --- /dev/null +++ b/src/modules/system/serve/serve.controller.ts @@ -0,0 +1,31 @@ +import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager'; +import { Controller, Get, UseInterceptors } from '@nestjs/common'; +import { ApiExtraModels, ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; + +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; + +import { AllowAnon } from '~/modules/auth/decorators/allow-anon.decorator'; + +import { ServeStatInfo } from './serve.model'; +import { ServeService } from './serve.service'; + +@ApiTags('System - 服务监控') +@ApiSecurityAuth() +@ApiExtraModels(ServeStatInfo) +@Controller('serve') +@UseInterceptors(CacheInterceptor) +@CacheKey('serve_stat') +@CacheTTL(10000) +export class ServeController { + constructor(private serveService: ServeService) {} + + @Get('stat') + @ApiOperation({ summary: '获取服务器运行信息' }) + @ApiResult({ type: ServeStatInfo }) + @AllowAnon() + async stat(): Promise { + return this.serveService.getServeStat(); + } +} diff --git a/src/modules/system/serve/serve.model.ts b/src/modules/system/serve/serve.model.ts new file mode 100644 index 0000000..1fba434 --- /dev/null +++ b/src/modules/system/serve/serve.model.ts @@ -0,0 +1,86 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class Runtime { + @ApiProperty({ description: '系统' }) + os?: string; + + @ApiProperty({ description: '服务器架构' }) + arch?: string; + + @ApiProperty({ description: 'Node版本' }) + nodeVersion?: string; + + @ApiProperty({ description: 'Npm版本' }) + npmVersion?: string; +} + +export class CoreLoad { + @ApiProperty({ description: '当前CPU资源消耗' }) + rawLoad?: number; + + @ApiProperty({ description: '当前空闲CPU资源' }) + rawLoadIdle?: number; +} + +// Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz +export class Cpu { + @ApiProperty({ description: '制造商' }) + manufacturer?: string; + + @ApiProperty({ description: '品牌' }) + brand?: string; + + @ApiProperty({ description: '物理核心数' }) + physicalCores?: number; + + @ApiProperty({ description: '型号' }) + model?: string; + + @ApiProperty({ description: '速度 in GHz' }) + speed?: number; + + @ApiProperty({ description: 'CPU资源消耗 原始滴答' }) + rawCurrentLoad?: number; + + @ApiProperty({ description: '空闲CPU资源 原始滴答' }) + rawCurrentLoadIdle?: number; + + @ApiProperty({ description: 'cpu资源消耗', type: [CoreLoad] }) + coresLoad?: CoreLoad[]; +} + +export class Disk { + @ApiProperty({ description: '磁盘空间大小 (bytes)' }) + size?: number; + + @ApiProperty({ description: '已使用磁盘空间 (bytes)' }) + used?: number; + + @ApiProperty({ description: '可用磁盘空间 (bytes)' }) + available?: number; +} + +export class Memory { + @ApiProperty({ description: 'total memory in bytes' }) + total?: number; + + @ApiProperty({ description: '可用内存' }) + available?: number; +} + +/** + * 系统信息 + */ +export class ServeStatInfo { + @ApiProperty({ description: '运行环境', type: Runtime }) + runtime?: Runtime; + + @ApiProperty({ description: 'CPU信息', type: Cpu }) + cpu?: Cpu; + + @ApiProperty({ description: '磁盘信息', type: Disk }) + disk?: Disk; + + @ApiProperty({ description: '内存信息', type: Memory }) + memory?: Memory; +} diff --git a/src/modules/system/serve/serve.module.ts b/src/modules/system/serve/serve.module.ts new file mode 100644 index 0000000..4a2f421 --- /dev/null +++ b/src/modules/system/serve/serve.module.ts @@ -0,0 +1,16 @@ +import { Module, forwardRef } from '@nestjs/common'; + +import { SystemModule } from '../system.module'; + +import { ServeController } from './serve.controller'; +import { ServeService } from './serve.service'; + +const providers = [ServeService]; + +@Module({ + imports: [forwardRef(() => SystemModule)], + controllers: [ServeController], + providers: [...providers], + exports: [...providers] +}) +export class ServeModule {} diff --git a/src/modules/system/serve/serve.service.ts b/src/modules/system/serve/serve.service.ts new file mode 100644 index 0000000..59b7ba1 --- /dev/null +++ b/src/modules/system/serve/serve.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@nestjs/common'; +import * as si from 'systeminformation'; + +import { Disk, ServeStatInfo } from './serve.model'; + +@Injectable() +export class ServeService { + /** + * 获取服务器信息 + */ + async getServeStat(): Promise { + const [versions, osinfo, cpuinfo, currentLoadinfo, meminfo] = ( + await Promise.allSettled([ + si.versions('node, npm'), + si.osInfo(), + si.cpu(), + si.currentLoad(), + si.mem() + ]) + ).map((p: any) => p.value); + + // 计算总空间 + const diskListInfo = await si.fsSize(); + const diskinfo = new Disk(); + diskinfo.size = 0; + diskinfo.available = 0; + diskinfo.used = 0; + diskListInfo.forEach(d => { + diskinfo.size += d.size; + diskinfo.available += d.available; + diskinfo.used += d.used; + }); + + return { + runtime: { + npmVersion: versions.npm, + nodeVersion: versions.node, + os: osinfo.platform, + arch: osinfo.arch + }, + cpu: { + manufacturer: cpuinfo.manufacturer, + brand: cpuinfo.brand, + physicalCores: cpuinfo.physicalCores, + model: cpuinfo.model, + speed: cpuinfo.speed, + rawCurrentLoad: currentLoadinfo.rawCurrentLoad, + rawCurrentLoadIdle: currentLoadinfo.rawCurrentLoadIdle, + coresLoad: currentLoadinfo.cpus.map(e => { + return { + rawLoad: e.rawLoad, + rawLoadIdle: e.rawLoadIdle + }; + }) + }, + disk: diskinfo, + memory: { + total: meminfo.total, + available: meminfo.available + } + }; + } +} diff --git a/src/modules/system/system.module.ts b/src/modules/system/system.module.ts new file mode 100644 index 0000000..83d9189 --- /dev/null +++ b/src/modules/system/system.module.ts @@ -0,0 +1,45 @@ +import { Module } from '@nestjs/common'; + +import { RouterModule } from '@nestjs/core'; + +import { UserModule } from '../user/user.module'; + +import { DeptModule } from './dept/dept.module'; +import { DictItemModule } from './dict-item/dict-item.module'; +import { DictTypeModule } from './dict-type/dict-type.module'; +import { LogModule } from './log/log.module'; +import { MenuModule } from './menu/menu.module'; +import { OnlineModule } from './online/online.module'; +import { ParamConfigModule } from './param-config/param-config.module'; +import { RoleModule } from './role/role.module'; +import { ServeModule } from './serve/serve.module'; +import { TaskModule } from './task/task.module'; + +const modules = [ + UserModule, + RoleModule, + MenuModule, + DeptModule, + DictTypeModule, + DictItemModule, + ParamConfigModule, + LogModule, + TaskModule, + OnlineModule, + ServeModule +]; + +@Module({ + imports: [ + ...modules, + RouterModule.register([ + { + path: 'system', + module: SystemModule, + children: [...modules] + } + ]) + ], + exports: [...modules] +}) +export class SystemModule {} diff --git a/src/modules/system/task/constant.ts b/src/modules/system/task/constant.ts new file mode 100644 index 0000000..0dac2d1 --- /dev/null +++ b/src/modules/system/task/constant.ts @@ -0,0 +1,12 @@ +export enum TaskStatus { + Disabled = 0, + Activited = 1 +} + +export enum TaskType { + Cron = 0, + Interval = 1 +} + +export const SYS_TASK_QUEUE_NAME = 'system:sys-task'; +export const SYS_TASK_QUEUE_PREFIX = 'system:sys:task'; diff --git a/src/modules/system/task/task.controller.ts b/src/modules/system/task/task.controller.ts new file mode 100644 index 0000000..e0c7a92 --- /dev/null +++ b/src/modules/system/task/task.controller.ts @@ -0,0 +1,98 @@ +import { Body, Controller, Delete, Get, Post, Put, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { Pagination } from '~/helper/paginate/pagination'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { TaskEntity } from '~/modules/system/task/task.entity'; + +import { TaskDto, TaskQueryDto, TaskUpdateDto } from './task.dto'; +import { TaskService } from './task.service'; + +export const permissions = definePermission('system:task', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete', + + ONCE: 'once', + START: 'start', + STOP: 'stop' +} as const); + +@ApiTags('System - 任务调度模块') +@ApiSecurityAuth() +@Controller('tasks') +export class TaskController { + constructor(private taskService: TaskService) {} + + @Get() + @ApiOperation({ summary: '获取任务列表' }) + @ApiResult({ type: [TaskEntity], isPage: true }) + @Perm(permissions.LIST) + async list(@Query() dto: TaskQueryDto): Promise> { + return this.taskService.list(dto); + } + + @Post() + @ApiOperation({ summary: '添加任务' }) + @Perm(permissions.CREATE) + async create(@Body() dto: TaskDto): Promise { + const serviceCall = dto.service.split('.'); + await this.taskService.checkHasMissionMeta(serviceCall[0], serviceCall[1]); + await this.taskService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新任务' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: TaskUpdateDto): Promise { + const serviceCall = dto.service.split('.'); + await this.taskService.checkHasMissionMeta(serviceCall[0], serviceCall[1]); + await this.taskService.update(id, dto); + } + + @Get(':id') + @ApiOperation({ summary: '查询任务详细信息' }) + @ApiResult({ type: TaskEntity }) + @Perm(permissions.READ) + async info(@IdParam() id: number): Promise { + return this.taskService.info(id); + } + + @Delete(':id') + @ApiOperation({ summary: '删除任务' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + const task = await this.taskService.info(id); + await this.taskService.delete(task); + } + + @Put(':id/once') + @ApiOperation({ summary: '手动执行一次任务' }) + @Perm(permissions.ONCE) + async once(@IdParam() id: number): Promise { + const task = await this.taskService.info(id); + await this.taskService.once(task); + } + + @Put(':id/stop') + @ApiOperation({ summary: '停止任务' }) + @Perm(permissions.STOP) + async stop(@IdParam() id: number): Promise { + const task = await this.taskService.info(id); + await this.taskService.stop(task); + } + + @Put(':id/start') + @ApiOperation({ summary: '启动任务' }) + @Perm(permissions.START) + async start(@IdParam() id: number): Promise { + const task = await this.taskService.info(id); + + await this.taskService.start(task); + } +} diff --git a/src/modules/system/task/task.dto.ts b/src/modules/system/task/task.dto.ts new file mode 100644 index 0000000..62ab7e6 --- /dev/null +++ b/src/modules/system/task/task.dto.ts @@ -0,0 +1,103 @@ +import { BadRequestException } from '@nestjs/common'; +import { ApiProperty, ApiPropertyOptional, IntersectionType, PartialType } from '@nestjs/swagger'; +import { + IsDateString, + IsIn, + IsInt, + IsOptional, + IsString, + MaxLength, + Min, + MinLength, + Validate, + ValidateIf, + ValidationArguments, + ValidatorConstraint, + ValidatorConstraintInterface +} from 'class-validator'; +import * as parser from 'cron-parser'; +import { isEmpty } from 'lodash'; + +import { PagerDto } from '~/common/dto/pager.dto'; + +// cron 表达式验证,bull lib下引用了cron-parser +@ValidatorConstraint({ name: 'isCronExpression', async: false }) +export class IsCronExpression implements ValidatorConstraintInterface { + validate(value: string, _args: ValidationArguments) { + try { + if (isEmpty(value)) throw new BadRequestException('cron expression is empty'); + + parser.parseExpression(value); + return true; + } catch (e) { + return false; + } + } + + defaultMessage(_args: ValidationArguments) { + return 'this cron expression ($value) invalid'; + } +} + +export class TaskDto { + @ApiProperty({ description: '任务名称' }) + @IsString() + @MinLength(2) + @MaxLength(50) + name: string; + + @ApiProperty({ description: '调用的服务' }) + @IsString() + @MinLength(1) + service: string; + + @ApiProperty({ description: '任务类别:cron | interval' }) + @IsIn([0, 1]) + type: number; + + @ApiProperty({ description: '任务状态' }) + @IsIn([0, 1]) + status: number; + + @ApiPropertyOptional({ description: '开始时间', type: Date }) + @IsDateString() + @ValidateIf(o => !isEmpty(o.startTime)) + startTime: string; + + @ApiPropertyOptional({ description: '结束时间', type: Date }) + @IsDateString() + @ValidateIf(o => !isEmpty(o.endTime)) + endTime: string; + + @ApiPropertyOptional({ + description: '限制执行次数,负数则无限制' + }) + @IsOptional() + @IsInt() + limit?: number = -1; + + @ApiProperty({ description: 'cron表达式' }) + @Validate(IsCronExpression) + @ValidateIf(o => o.type === 0) + cron: string; + + @ApiProperty({ description: '执行间隔,毫秒单位' }) + @IsInt() + @Min(100) + @ValidateIf(o => o.type === 1) + every?: number; + + @ApiPropertyOptional({ description: '执行参数' }) + @IsOptional() + @IsString() + data?: string; + + @ApiPropertyOptional({ description: '任务备注' }) + @IsOptional() + @IsString() + remark?: string; +} + +export class TaskUpdateDto extends PartialType(TaskDto) {} + +export class TaskQueryDto extends IntersectionType(PagerDto, PartialType(TaskDto)) {} diff --git a/src/modules/system/task/task.entity.ts b/src/modules/system/task/task.entity.ts new file mode 100644 index 0000000..29738f6 --- /dev/null +++ b/src/modules/system/task/task.entity.ts @@ -0,0 +1,55 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Column, Entity } from 'typeorm'; + +import { CommonEntity } from '~/common/entity/common.entity'; + +@Entity({ name: 'sys_task' }) +export class TaskEntity extends CommonEntity { + @Column({ type: 'varchar', length: 50, unique: true }) + @ApiProperty({ description: '任务名' }) + name: string; + + @Column() + @ApiProperty({ description: '任务标识' }) + service: string; + + @Column({ type: 'tinyint', default: 0 }) + @ApiProperty({ description: '任务类型 0cron 1间隔' }) + type: number; + + @Column({ type: 'tinyint', default: 1 }) + @ApiProperty({ description: '任务状态 0禁用 1启用' }) + status: number; + + @Column({ name: 'start_time', type: 'datetime', nullable: true }) + @ApiProperty({ description: '开始时间' }) + startTime: Date; + + @Column({ name: 'end_time', type: 'datetime', nullable: true }) + @ApiProperty({ description: '结束时间' }) + endTime: Date; + + @Column({ type: 'int', nullable: true, default: 0 }) + @ApiProperty({ description: '间隔时间' }) + limit: number; + + @Column({ nullable: true }) + @ApiProperty({ description: 'cron表达式' }) + cron: string; + + @Column({ type: 'int', nullable: true }) + @ApiProperty({ description: '执行次数' }) + every: number; + + @Column({ type: 'text', nullable: true }) + @ApiProperty({ description: '任务参数' }) + data: string; + + @Column({ name: 'job_opts', type: 'text', nullable: true }) + @ApiProperty({ description: '任务配置' }) + jobOpts: string; + + @Column({ nullable: true }) + @ApiProperty({ description: '任务描述' }) + remark: string; +} diff --git a/src/modules/system/task/task.module.ts b/src/modules/system/task/task.module.ts new file mode 100644 index 0000000..5aba9a3 --- /dev/null +++ b/src/modules/system/task/task.module.ts @@ -0,0 +1,37 @@ +import { BullModule } from '@nestjs/bull'; +import { Module } from '@nestjs/common'; + +import { ConfigService } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { ConfigKeyPaths, IRedisConfig } from '~/config'; + +import { LogModule } from '../log/log.module'; + +import { SYS_TASK_QUEUE_NAME, SYS_TASK_QUEUE_PREFIX } from './constant'; + +import { TaskController } from './task.controller'; +import { TaskEntity } from './task.entity'; +import { TaskConsumer } from './task.processor'; +import { TaskService } from './task.service'; + +const providers = [TaskService, TaskConsumer]; + +@Module({ + imports: [ + TypeOrmModule.forFeature([TaskEntity]), + BullModule.registerQueueAsync({ + name: SYS_TASK_QUEUE_NAME, + useFactory: (configService: ConfigService) => ({ + redis: configService.get('redis'), + prefix: SYS_TASK_QUEUE_PREFIX + }), + inject: [ConfigService] + }), + LogModule + ], + controllers: [TaskController], + providers: [...providers], + exports: [TypeOrmModule, ...providers] +}) +export class TaskModule {} diff --git a/src/modules/system/task/task.processor.ts b/src/modules/system/task/task.processor.ts new file mode 100644 index 0000000..57147ab --- /dev/null +++ b/src/modules/system/task/task.processor.ts @@ -0,0 +1,43 @@ +import { OnQueueCompleted, Process, Processor } from '@nestjs/bull'; +import { Job } from 'bull'; + +import { TaskLogService } from '../log/services/task-log.service'; + +import { SYS_TASK_QUEUE_NAME } from './constant'; + +import { TaskService } from './task.service'; + +export interface ExecuteData { + id: number; + args?: string | null; + service: string; +} + +@Processor(SYS_TASK_QUEUE_NAME) +export class TaskConsumer { + constructor( + private taskService: TaskService, + private taskLogService: TaskLogService + ) {} + + @Process() + async handle(job: Job): Promise { + const startTime = Date.now(); + const { data } = job; + try { + await this.taskService.callService(data.service, data.args); + const timing = Date.now() - startTime; + // 任务执行成功 + await this.taskLogService.create(data.id, 1, timing); + } catch (e) { + const timing = Date.now() - startTime; + // 执行失败 + await this.taskLogService.create(data.id, 0, timing, `${e}`); + } + } + + @OnQueueCompleted() + onCompleted(job: Job) { + this.taskService.updateTaskCompleteStatus(job.data.id); + } +} diff --git a/src/modules/system/task/task.service.ts b/src/modules/system/task/task.service.ts new file mode 100644 index 0000000..aa59bf5 --- /dev/null +++ b/src/modules/system/task/task.service.ts @@ -0,0 +1,332 @@ +import { InjectRedis } from '@liaoliaots/nestjs-redis'; +import { InjectQueue } from '@nestjs/bull'; +import { + BadRequestException, + Injectable, + Logger, + NotFoundException, + OnModuleInit +} from '@nestjs/common'; +import { ModuleRef, Reflector } from '@nestjs/core'; +import { UnknownElementException } from '@nestjs/core/errors/exceptions/unknown-element.exception'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Queue } from 'bull'; +import Redis from 'ioredis'; +import { isEmpty, isNumber } from 'lodash'; +import { Like, Repository } from 'typeorm'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; + +import { paginate } from '~/helper/paginate'; +import { Pagination } from '~/helper/paginate/pagination'; + +import { TaskEntity } from '~/modules/system/task/task.entity'; +import { MISSION_DECORATOR_KEY } from '~/modules/tasks/mission.decorator'; + +import { SYS_TASK_QUEUE_NAME, SYS_TASK_QUEUE_PREFIX, TaskStatus } from './constant'; +import { TaskDto, TaskQueryDto, TaskUpdateDto } from './task.dto'; + +@Injectable() +export class TaskService implements OnModuleInit { + private logger = new Logger(TaskService.name); + + constructor( + @InjectRepository(TaskEntity) + private taskRepository: Repository, + @InjectQueue(SYS_TASK_QUEUE_NAME) private taskQueue: Queue, + private moduleRef: ModuleRef, + private reflector: Reflector, + @InjectRedis() private redis: Redis + ) {} + + /** + * module init + */ + async onModuleInit() { + await this.initTask(); + } + + /** + * 初始化任务,系统启动前调用 + */ + async initTask(): Promise { + const initKey = `${SYS_TASK_QUEUE_PREFIX}:init`; + // 防止重复初始化 + const result = await this.redis + .multi() + .setnx(initKey, new Date().getTime()) + .expire(initKey, 60 * 30) + .exec(); + if (result[0][1] === 0) { + // 存在锁则直接跳过防止重复初始化 + this.logger.log('Init task is lock', TaskService.name); + return; + } + const jobs = await this.taskQueue.getJobs([ + 'active', + 'delayed', + 'failed', + 'paused', + 'waiting', + 'completed' + ]); + jobs.forEach(j => { + j.remove(); + }); + + // 查找所有需要运行的任务 + const tasks = await this.taskRepository.findBy({ status: 1 }); + if (tasks && tasks.length > 0) { + for (const t of tasks) await this.start(t); + } + // 启动后释放锁 + await this.redis.del(initKey); + } + + async list({ + page, + pageSize, + name, + service, + type, + status + }: TaskQueryDto): Promise> { + const queryBuilder = this.taskRepository + .createQueryBuilder('task') + .where({ + ...(name ? { name: Like(`%${name}%`) } : null), + ...(service ? { service: Like(`%${service}%`) } : null), + ...(type ? { type } : null), + ...(isNumber(status) ? { status } : null) + }) + .orderBy('task.id', 'ASC'); + + return paginate(queryBuilder, { page, pageSize }); + } + + /** + * task info + */ + async info(id: number): Promise { + const task = this.taskRepository.createQueryBuilder('task').where({ id }).getOne(); + + if (!task) throw new NotFoundException('Task Not Found'); + + return task; + } + + /** + * delete task + */ + async delete(task: TaskEntity): Promise { + if (!task) throw new BadRequestException('Task is Empty'); + + await this.stop(task); + await this.taskRepository.delete(task.id); + } + + /** + * 手动执行一次 + */ + async once(task: TaskEntity): Promise { + if (task) { + await this.taskQueue.add( + { id: task.id, service: task.service, args: task.data }, + { jobId: task.id, removeOnComplete: true, removeOnFail: true } + ); + } else { + throw new BadRequestException('Task is Empty'); + } + } + + async create(dto: TaskDto): Promise { + const result = await this.taskRepository.save(dto); + const task = await this.info(result.id); + if (result.status === 0) await this.stop(task); + else if (result.status === TaskStatus.Activited) await this.start(task); + } + + async update(id: number, dto: TaskUpdateDto): Promise { + await this.taskRepository.update(id, dto); + const task = await this.info(id); + if (task.status === 0) await this.stop(task); + else if (task.status === TaskStatus.Activited) await this.start(task); + } + + /** + * 启动任务 + */ + async start(task: TaskEntity): Promise { + if (!task) throw new BadRequestException('Task is Empty'); + + // 先停掉之前存在的任务 + await this.stop(task); + let repeat: any; + if (task.type === 1) { + // 间隔 Repeat every millis (cron setting cannot be used together with this setting.) + repeat = { + every: task.every + }; + } else { + // cron + repeat = { + cron: task.cron + }; + // Start date when the repeat job should start repeating (only with cron). + if (task.startTime) repeat.startDate = task.startTime; + + if (task.endTime) repeat.endDate = task.endTime; + } + if (task.limit > 0) repeat.limit = task.limit; + + const job = await this.taskQueue.add( + { id: task.id, service: task.service, args: task.data }, + { jobId: task.id, removeOnComplete: true, removeOnFail: true, repeat } + ); + if (job && job.opts) { + await this.taskRepository.update(task.id, { + jobOpts: JSON.stringify(job.opts.repeat), + status: 1 + }); + } else { + // update status to 0,标识暂停任务,因为启动失败 + await job?.remove(); + await this.taskRepository.update(task.id, { + status: TaskStatus.Disabled + }); + throw new BadRequestException('Task Start failed'); + } + } + + /** + * 停止任务 + */ + async stop(task: TaskEntity): Promise { + if (!task) throw new BadRequestException('Task is Empty'); + + const exist = await this.existJob(task.id.toString()); + if (!exist) { + await this.taskRepository.update(task.id, { + status: TaskStatus.Disabled + }); + return; + } + const jobs = await this.taskQueue.getJobs([ + 'active', + 'delayed', + 'failed', + 'paused', + 'waiting', + 'completed' + ]); + jobs + .filter(j => j.data.id === task.id) + .forEach(async j => { + await j.remove(); + }); + + await this.taskRepository.update(task.id, { status: TaskStatus.Disabled }); + // if (task.jobOpts) { + // await this.app.queue.sys.removeRepeatable(JSON.parse(task.jobOpts)); + // // update status + // await this.getRepo().admin.sys.Task.update(task.id, { status: TaskStatus.Disabled, }); + // } + } + + /** + * 查看队列中任务是否存在 + */ + async existJob(jobId: string): Promise { + // https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md#queueremoverepeatablebykey + const jobs = await this.taskQueue.getRepeatableJobs(); + const ids = jobs.map(e => { + return e.id; + }); + return ids.includes(jobId); + } + + /** + * 更新是否已经完成,完成则移除该任务并修改状态 + */ + async updateTaskCompleteStatus(tid: number): Promise { + const jobs = await this.taskQueue.getRepeatableJobs(); + const task = await this.taskRepository.findOneBy({ id: tid }); + // 如果下次执行时间小于当前时间,则表示已经执行完成。 + for (const job of jobs) { + const currentTime = new Date().getTime(); + if (job.id === tid.toString() && job.next < currentTime) { + // 如果下次执行时间小于当前时间,则表示已经执行完成。 + await this.stop(task); + break; + } + } + } + + /** + * 检测service是否有注解定义 + * @param serviceName service + */ + async checkHasMissionMeta(nameOrInstance: string | unknown, exec: string): Promise { + try { + let service: any; + if (typeof nameOrInstance === 'string') + service = await this.moduleRef.get(nameOrInstance, { strict: false }); + else service = nameOrInstance; + + // 所执行的任务不存在 + if (!service || !(exec in service)) throw new NotFoundException('任务不存在'); + + // 检测是否有Mission注解 + const hasMission = this.reflector.get(MISSION_DECORATOR_KEY, service.constructor); + // 如果没有,则抛出错误 + if (!hasMission) throw new BusinessException(ErrorEnum.INSECURE_MISSION); + } catch (e) { + if (e instanceof UnknownElementException) { + // 任务不存在 + throw new NotFoundException('任务不存在'); + } else { + // 其余错误则不处理,继续抛出 + throw e; + } + } + } + + /** + * 根据serviceName调用service,例如 LogService.clearReqLog + */ + async callService(name: string, args: string): Promise { + if (name) { + const [serviceName, methodName] = name.split('.'); + if (!methodName) throw new BadRequestException('serviceName define BadRequestException'); + + const service = await this.moduleRef.get(serviceName, { + strict: false + }); + + // 安全注解检查 + await this.checkHasMissionMeta(service, methodName); + if (isEmpty(args)) { + await service[methodName](); + } else { + // 参数安全判断 + const parseArgs = this.safeParse(args); + + if (Array.isArray(parseArgs)) { + // 数组形式则自动扩展成方法参数回掉 + await service[methodName](...parseArgs); + } else { + await service[methodName](parseArgs); + } + } + } + } + + safeParse(args: string): unknown | string { + try { + return JSON.parse(args); + } catch (e) { + return args; + } + } +} diff --git a/src/modules/system/task/task.ts b/src/modules/system/task/task.ts new file mode 100644 index 0000000..10c09b3 --- /dev/null +++ b/src/modules/system/task/task.ts @@ -0,0 +1,2 @@ +export const SYS_TASK_QUEUE_NAME = 'system:sys-task'; +export const SYS_TASK_QUEUE_PREFIX = 'system:sys:task'; diff --git a/src/modules/tasks/jobs/email.job.ts b/src/modules/tasks/jobs/email.job.ts new file mode 100644 index 0000000..be1bc61 --- /dev/null +++ b/src/modules/tasks/jobs/email.job.ts @@ -0,0 +1,28 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; + +import { LoggerService } from '~/shared/logger/logger.service'; +import { MailerService } from '~/shared/mailer/mailer.service'; + +import { Mission } from '../mission.decorator'; + +/** + * Api接口请求类型任务 + */ +@Injectable() +@Mission() +export class EmailJob { + constructor( + private readonly emailService: MailerService, + private readonly logger: LoggerService + ) {} + + async send(config: any): Promise { + if (config) { + const { to, subject, content } = config; + const result = await this.emailService.send(to, subject, content); + this.logger.log(result, EmailJob.name); + } else { + throw new BadRequestException('Email send job param is empty'); + } + } +} diff --git a/src/modules/tasks/jobs/http-request.job.ts b/src/modules/tasks/jobs/http-request.job.ts new file mode 100644 index 0000000..7922913 --- /dev/null +++ b/src/modules/tasks/jobs/http-request.job.ts @@ -0,0 +1,31 @@ +import { HttpService } from '@nestjs/axios'; +import { BadRequestException, Injectable } from '@nestjs/common'; + +import { LoggerService } from '~/shared/logger/logger.service'; + +import { Mission } from '../mission.decorator'; + +/** + * Api接口请求类型任务 + */ +@Injectable() +@Mission() +export class HttpRequestJob { + constructor( + private readonly httpService: HttpService, + private readonly logger: LoggerService + ) {} + + /** + * 发起请求 + * @param config {AxiosRequestConfig} + */ + async handle(config: unknown): Promise { + if (config) { + const result = await this.httpService.request(config); + this.logger.log(result, HttpRequestJob.name); + } else { + throw new BadRequestException('Http request job param is empty'); + } + } +} diff --git a/src/modules/tasks/jobs/log-clear.job.ts b/src/modules/tasks/jobs/log-clear.job.ts new file mode 100644 index 0000000..dc24f5d --- /dev/null +++ b/src/modules/tasks/jobs/log-clear.job.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; + +import { LoginLogService } from '~/modules/system/log/services/login-log.service'; +import { TaskLogService } from '~/modules/system/log/services/task-log.service'; + +import { Mission } from '../mission.decorator'; + +/** + * 管理后台日志清理任务 + */ +@Injectable() +@Mission() +export class LogClearJob { + constructor( + private loginLogService: LoginLogService, + private taskLogService: TaskLogService + ) {} + + async clearLoginLog(): Promise { + await this.loginLogService.clearLog(); + } + + async clearTaskLog(): Promise { + await this.taskLogService.clearLog(); + } +} diff --git a/src/modules/tasks/mission.decorator.ts b/src/modules/tasks/mission.decorator.ts new file mode 100644 index 0000000..beef1b1 --- /dev/null +++ b/src/modules/tasks/mission.decorator.ts @@ -0,0 +1,8 @@ +import { SetMetadata } from '@nestjs/common'; + +export const MISSION_DECORATOR_KEY = 'decorator:mission'; + +/** + * 定时任务标记,没有该任务标记的任务不会被执行,保证全局获取下的模块被安全执行 + */ +export const Mission = () => SetMetadata(MISSION_DECORATOR_KEY, true); diff --git a/src/modules/tasks/tasks.module.ts b/src/modules/tasks/tasks.module.ts new file mode 100644 index 0000000..c422124 --- /dev/null +++ b/src/modules/tasks/tasks.module.ts @@ -0,0 +1,46 @@ +import { DynamicModule, ExistingProvider, Module } from '@nestjs/common'; + +import { LogModule } from '~/modules/system/log/log.module'; +import { SystemModule } from '~/modules/system/system.module'; + +import { EmailJob } from './jobs/email.job'; +import { HttpRequestJob } from './jobs/http-request.job'; +import { LogClearJob } from './jobs/log-clear.job'; + +const providers = [LogClearJob, HttpRequestJob, EmailJob]; + +/** + * auto create alias + * { + * provide: 'LogClearMissionService', + * useExisting: LogClearMissionService, + * } + */ +function createAliasProviders(): ExistingProvider[] { + const aliasProviders: ExistingProvider[] = []; + for (const p of providers) { + aliasProviders.push({ + provide: p.name, + useExisting: p + }); + } + return aliasProviders; +} + +/** + * 所有需要执行的定时任务都需要在这里注册 + */ +@Module({}) +export class TasksModule { + static forRoot(): DynamicModule { + // 使用Alias定义别名,使得可以通过字符串类型获取定义的Service,否则无法获取 + const aliasProviders = createAliasProviders(); + return { + global: true, + module: TasksModule, + imports: [SystemModule, LogModule], + providers: [...providers, ...aliasProviders], + exports: aliasProviders + }; + } +} diff --git a/src/modules/todo/todo.controller.ts b/src/modules/todo/todo.controller.ts new file mode 100644 index 0000000..d1802a0 --- /dev/null +++ b/src/modules/todo/todo.controller.ts @@ -0,0 +1,69 @@ +import { Body, Controller, Delete, Get, Post, Put, Query, UseGuards } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; + +import { Pagination } from '~/helper/paginate/pagination'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { Resource } from '~/modules/auth/decorators/resource.decorator'; + +import { ResourceGuard } from '~/modules/auth/guards/resource.guard'; +import { TodoEntity } from '~/modules/todo/todo.entity'; + +import { TodoDto, TodoQueryDto, TodoUpdateDto } from './todo.dto'; +import { TodoService } from './todo.service'; + +export const permissions = definePermission('todo', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('Business - Todo模块') +@UseGuards(ResourceGuard) +@Controller('todos') +export class TodoController { + constructor(private readonly todoService: TodoService) {} + + @Get() + @ApiOperation({ summary: '获取Todo列表' }) + @ApiResult({ type: [TodoEntity] }) + @Perm(permissions.LIST) + async list(@Query() dto: TodoQueryDto): Promise> { + return this.todoService.list(dto); + } + + @Get(':id') + @ApiOperation({ summary: '获取Todo详情' }) + @ApiResult({ type: TodoEntity }) + @Perm(permissions.READ) + async info(@IdParam() id: number): Promise { + return this.todoService.detail(id); + } + + @Post() + @ApiOperation({ summary: '创建Todo' }) + @Perm(permissions.CREATE) + async create(@Body() dto: TodoDto): Promise { + await this.todoService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新Todo' }) + @Perm(permissions.UPDATE) + @Resource(TodoEntity) + async update(@IdParam() id: number, @Body() dto: TodoUpdateDto): Promise { + await this.todoService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除Todo' }) + @Perm(permissions.DELETE) + @Resource(TodoEntity) + async delete(@IdParam() id: number): Promise { + await this.todoService.delete(id); + } +} diff --git a/src/modules/todo/todo.dto.ts b/src/modules/todo/todo.dto.ts new file mode 100644 index 0000000..9554651 --- /dev/null +++ b/src/modules/todo/todo.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +import { PagerDto } from '~/common/dto/pager.dto'; + +export class TodoDto { + @ApiProperty({ description: '名称' }) + @IsString() + value: string; +} + +export class TodoUpdateDto extends PartialType(TodoDto) {} + +export class TodoQueryDto extends IntersectionType(PagerDto, TodoDto) {} diff --git a/src/modules/todo/todo.entity.ts b/src/modules/todo/todo.entity.ts new file mode 100644 index 0000000..41b8ffb --- /dev/null +++ b/src/modules/todo/todo.entity.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinColumn, ManyToOne, Relation } from 'typeorm'; + +import { CommonEntity } from '~/common/entity/common.entity'; +import { UserEntity } from '~/modules/user/user.entity'; + +@Entity('todo') +export class TodoEntity extends CommonEntity { + @Column() + @ApiProperty({ description: 'todo' }) + value: string; + + @ApiProperty({ description: 'todo' }) + @Column({ default: false }) + status: boolean; + + @ManyToOne(() => UserEntity) + @JoinColumn({ name: 'user_id' }) + user: Relation; +} diff --git a/src/modules/todo/todo.module.ts b/src/modules/todo/todo.module.ts new file mode 100644 index 0000000..28a7b3e --- /dev/null +++ b/src/modules/todo/todo.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { TodoController } from './todo.controller'; +import { TodoEntity } from './todo.entity'; +import { TodoService } from './todo.service'; + +const services = [TodoService]; + +@Module({ + imports: [TypeOrmModule.forFeature([TodoEntity])], + controllers: [TodoController], + providers: [...services], + exports: [TypeOrmModule, ...services] +}) +export class TodoModule {} diff --git a/src/modules/todo/todo.service.ts b/src/modules/todo/todo.service.ts new file mode 100644 index 0000000..26b4e23 --- /dev/null +++ b/src/modules/todo/todo.service.ts @@ -0,0 +1,42 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { paginate } from '~/helper/paginate'; +import { Pagination } from '~/helper/paginate/pagination'; +import { TodoEntity } from '~/modules/todo/todo.entity'; + +import { TodoDto, TodoQueryDto, TodoUpdateDto } from './todo.dto'; + +@Injectable() +export class TodoService { + constructor( + @InjectRepository(TodoEntity) + private todoRepository: Repository + ) {} + + async list({ page, pageSize }: TodoQueryDto): Promise> { + return paginate(this.todoRepository, { page, pageSize }); + } + + async detail(id: number): Promise { + const item = await this.todoRepository.findOneBy({ id }); + if (!item) throw new NotFoundException('未找到该记录'); + + return item; + } + + async create(dto: TodoDto) { + await this.todoRepository.save(dto); + } + + async update(id: number, dto: TodoUpdateDto) { + await this.todoRepository.update(id, dto); + } + + async delete(id: number) { + const item = await this.detail(id); + + await this.todoRepository.remove(item); + } +} diff --git a/src/modules/tools/email/email.controller.ts b/src/modules/tools/email/email.controller.ts new file mode 100644 index 0000000..6afde9d --- /dev/null +++ b/src/modules/tools/email/email.controller.ts @@ -0,0 +1,22 @@ +import { Body, Controller, Post } from '@nestjs/common'; + +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { MailerService } from '~/shared/mailer/mailer.service'; + +import { EmailSendDto } from './email.dto'; + +@ApiTags('System - 邮箱模块') +@ApiSecurityAuth() +@Controller('email') +export class EmailController { + constructor(private emailService: MailerService) {} + + @ApiOperation({ summary: '发送邮件' }) + @Post('send') + async send(@Body() dto: EmailSendDto): Promise { + const { to, subject, content } = dto; + await this.emailService.send(to, subject, content, 'html'); + } +} diff --git a/src/modules/tools/email/email.dto.ts b/src/modules/tools/email/email.dto.ts new file mode 100644 index 0000000..b6cd598 --- /dev/null +++ b/src/modules/tools/email/email.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsString } from 'class-validator'; + +/** + * 发送邮件 + */ +export class EmailSendDto { + @ApiProperty({ description: '收件人邮箱' }) + @IsEmail() + to: string; + + @ApiProperty({ description: '标题' }) + @IsString() + subject: string; + + @ApiProperty({ description: '正文' }) + @IsString() + content: string; +} diff --git a/src/modules/tools/email/email.module.ts b/src/modules/tools/email/email.module.ts new file mode 100644 index 0000000..504c1de --- /dev/null +++ b/src/modules/tools/email/email.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { EmailController } from './email.controller'; + +@Module({ + imports: [], + controllers: [EmailController] +}) +export class EmailModule {} diff --git a/src/modules/tools/storage/storage.controller.ts b/src/modules/tools/storage/storage.controller.ts new file mode 100644 index 0000000..0be18f2 --- /dev/null +++ b/src/modules/tools/storage/storage.controller.ts @@ -0,0 +1,41 @@ +import { Body, Controller, Get, Post, Query } from '@nestjs/common'; + +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; + +import { Pagination } from '~/helper/paginate/pagination'; + +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; + +import { StorageDeleteDto, StoragePageDto } from './storage.dto'; +import { StorageInfo } from './storage.modal'; +import { StorageService } from './storage.service'; + +export const permissions = definePermission('tool:storage', { + LIST: 'list', + DELETE: 'delete' +} as const); + +@ApiTags('Tools - 存储模块') +@ApiSecurityAuth() +@Controller('storage') +export class StorageController { + constructor(private storageService: StorageService) {} + + @Get('list') + @ApiOperation({ summary: '获取本地存储列表' }) + @ApiResult({ type: [StorageInfo], isPage: true }) + @Perm(permissions.LIST) + async list(@Query() dto: StoragePageDto): Promise> { + return this.storageService.list(dto); + } + + @ApiOperation({ summary: '删除文件' }) + @Post('delete') + @Perm(permissions.DELETE) + async delete(@Body() dto: StorageDeleteDto): Promise { + await this.storageService.delete(dto.ids); + } +} diff --git a/src/modules/tools/storage/storage.dto.ts b/src/modules/tools/storage/storage.dto.ts new file mode 100644 index 0000000..2ef2924 --- /dev/null +++ b/src/modules/tools/storage/storage.dto.ts @@ -0,0 +1,91 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { ArrayNotEmpty, IsArray, IsNumber, IsOptional, IsString } from 'class-validator'; + +import { PagerDto } from '~/common/dto/pager.dto'; + +export class StoragePageDto extends PagerDto { + @ApiProperty({ description: '文件名' }) + @IsOptional() + @IsString() + name: string; + + @ApiProperty({ description: '文件后缀' }) + @IsString() + @IsOptional() + extName: string; + + @ApiProperty({ description: '文件类型' }) + @IsString() + @IsOptional() + type: string; + + @ApiProperty({ description: '大小' }) + @IsString() + @IsOptional() + size: string; + + @ApiProperty({ description: '上传时间' }) + @IsOptional() + time: string[]; + + @ApiProperty({ description: '上传者' }) + @IsString() + @IsOptional() + username: string; + + @ApiProperty({ description: '文件所属模块' }) + @IsString() + @IsOptional() + businessModule: string; + + @ApiProperty({ description: '文件上传的业务记录ID' }) + @IsNumber() + @IsOptional() + bussinessRecordId: string; + + @ApiProperty({ description: '附件' }) + @IsOptional() + @Transform( + ({ value: val }) => { + return val ? val.split(',').map(item => Number(item)) : []; + }, + { + toClassOnly: true + } + ) + ids: number[]; +} + +export class StorageCreateDto { + @ApiProperty({ description: '文件名' }) + @IsString() + name: string; + + @ApiProperty({ description: '真实文件名' }) + @IsString() + fileName: string; + + @ApiProperty({ description: '文件扩展名' }) + @IsString() + extName: string; + + @ApiProperty({ description: '文件路径' }) + @IsString() + path: string; + + @ApiProperty({ description: '文件路径' }) + @IsString() + type: string; + + @ApiProperty({ description: '文件大小' }) + @IsString() + size: string; +} + +export class StorageDeleteDto { + @ApiProperty({ description: '需要删除的文件ID列表', type: [Number] }) + @IsArray() + @ArrayNotEmpty() + ids: number[]; +} diff --git a/src/modules/tools/storage/storage.entity.ts b/src/modules/tools/storage/storage.entity.ts new file mode 100644 index 0000000..ecdbad9 --- /dev/null +++ b/src/modules/tools/storage/storage.entity.ts @@ -0,0 +1,86 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, ManyToMany, Relation } from 'typeorm'; + +import { CommonEntity } from '~/common/entity/common.entity'; +import { CompanyEntity } from '~/modules/company/company.entity'; +import { ContractEntity } from '~/modules/contract/contract.entity'; +import { MaterialsInOutEntity } from '~/modules/materials_inventory/in_out/materials_in_out.entity'; +import { MaterialsInventoryEntity } from '~/modules/materials_inventory/materials_inventory.entity'; +import { ProductEntity } from '~/modules/product/product.entity'; +import { ProjectEntity } from '~/modules/project/project.entity'; +import { SaleQuotationComponentEntity } from '~/modules/sale_quotation/component/sale_quotation_component.entity'; +import { VehicleUsageEntity } from '~/modules/vehicle_usage/vehicle_usage.entity'; + +@Entity({ name: 'tool_storage' }) +export class Storage extends CommonEntity { + @Column({ type: 'varchar', length: 200, comment: '文件名' }) + @ApiProperty({ description: '文件名' }) + name: string; + + @Column({ + type: 'varchar', + length: 200, + nullable: true, + comment: '真实文件名' + }) + @ApiProperty({ description: '真实文件名' }) + fileName: string; + + @Column({ name: 'ext_name', type: 'varchar', nullable: true }) + @ApiProperty({ description: '扩展名' }) + extName: string; + + @Column({ type: 'varchar' }) + @ApiProperty({ description: '文件类型' }) + path: string; + + @Column({ type: 'varchar', nullable: true }) + @ApiProperty({ description: '文件类型' }) + type: string; + + @Column({ type: 'varchar', nullable: true }) + @ApiProperty({ description: '文件大小' }) + size: string; + + @Column({ nullable: true, name: 'user_id' }) + @ApiProperty({ description: '用户ID' }) + userId: number; + + @Column({ nullable: true, name: 'bussiness_module' }) + @ApiProperty({ description: '文件上传的业务模块' }) + bussinessModule: string; + + @Column({ nullable: true, name: 'bussiness_record_id' }) + @ApiProperty({ description: '文件上传的业务记录ID' }) + bussinessRecordId: number; + + + @ApiHideProperty() + @ManyToMany(() => ContractEntity, contract => contract.files) + contracts: Relation; + + @ApiHideProperty() + @ManyToMany(() => CompanyEntity, company => company.files) + companys: Relation; + + @ApiHideProperty() + @ManyToMany(() => MaterialsInOutEntity, materialsInOut => materialsInOut.files) + materialsInOuts: Relation; + + @ApiHideProperty() + @ManyToMany(() => ProductEntity, product => product.files) + products: Relation; + + @ApiHideProperty() + @ManyToMany(() => ProjectEntity, project => project.files) + projects: Relation; + + @ApiHideProperty() + @ManyToMany(() => VehicleUsageEntity, vu => vu.files) + vehicleUsage: Relation; + + @ApiHideProperty() + @ManyToMany(() => SaleQuotationComponentEntity, component => component.files) + saleQuotationComponents: Relation; + +} diff --git a/src/modules/tools/storage/storage.modal.ts b/src/modules/tools/storage/storage.modal.ts new file mode 100644 index 0000000..43ede4e --- /dev/null +++ b/src/modules/tools/storage/storage.modal.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class StorageInfo { + @ApiProperty({ description: '文件ID' }) + id: number; + + @ApiProperty({ description: '文件名' }) + name: string; + + @ApiProperty({ description: '文件扩展名' }) + extName: string; + + @ApiProperty({ description: '文件路径' }) + path: string; + + @ApiProperty({ description: '文件类型' }) + type: string; + + @ApiProperty({ description: '大小' }) + size: string; + + @ApiProperty({ description: '上传时间' }) + createdAt: string; + + @ApiProperty({ description: '上传者' }) + username: string; +} diff --git a/src/modules/tools/storage/storage.module.ts b/src/modules/tools/storage/storage.module.ts new file mode 100644 index 0000000..3324cf3 --- /dev/null +++ b/src/modules/tools/storage/storage.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { UserEntity } from '~/modules/user/user.entity'; + +import { StorageController } from './storage.controller'; +import { Storage } from './storage.entity'; +import { StorageService } from './storage.service'; + +const services = [StorageService]; + +@Module({ + imports: [TypeOrmModule.forFeature([Storage, UserEntity])], + controllers: [StorageController], + providers: [...services], + exports: [TypeOrmModule, ...services] +}) +export class StorageModule {} diff --git a/src/modules/tools/storage/storage.service.ts b/src/modules/tools/storage/storage.service.ts new file mode 100644 index 0000000..a24c122 --- /dev/null +++ b/src/modules/tools/storage/storage.service.ts @@ -0,0 +1,112 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { Between, EntityManager, In, Like, Repository } from 'typeorm'; + +import { paginateRaw } from '~/helper/paginate'; +import { PaginationTypeEnum } from '~/helper/paginate/interface'; +import { Pagination } from '~/helper/paginate/pagination'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { UserEntity } from '~/modules/user/user.entity'; +import { deleteFile } from '~/utils'; + +import { StorageCreateDto, StoragePageDto } from './storage.dto'; +import { StorageInfo } from './storage.modal'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; + +@Injectable() +export class StorageService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(Storage) + private storageRepository: Repository, + @InjectRepository(UserEntity) + private userRepository: Repository + ) {} + + async create(dto: StorageCreateDto, userId: number): Promise { + await this.storageRepository.save({ + ...dto, + userId + }); + } + + /** + * 删除文件 + */ + async delete(fileIds: number[]): Promise { + await this.entityManager.transaction(async manager => { + const items = await this.storageRepository.findBy({ id: In(fileIds) }); + try { + await manager.delete(Storage, fileIds); + items.forEach(el => { + deleteFile(el.path); + }); + } catch (e) { + if (e.code === 'ER_ROW_IS_REFERENCED_2') { + throw new BusinessException(ErrorEnum.STORAGE_REFRENCE_EXISTS); + } + } + }); + } + + async list({ + page, + pageSize, + name, + type, + size, + extName, + time, + username, + ids + }: StoragePageDto): Promise> { + const queryBuilder = this.storageRepository + .createQueryBuilder('storage') + .leftJoinAndSelect('sys_user', 'user', 'storage.user_id = user.id') + .where({ + ...(name && { name: Like(`%${name}%`) }), + ...(type && { type }), + ...(extName && { extName }), + ...(size && { size: Between(size[0], size[1]) }), + ...(time && { createdAt: Between(time[0], time[1]) }), + ...(username && { + userId: await (await this.userRepository.findOneBy({ username })).id + }), + ...(ids && { id: In(ids) }) + }) + .orderBy('storage.created_at', 'DESC'); + + const { items, ...rest } = await paginateRaw(queryBuilder, { + page, + pageSize, + paginationType: PaginationTypeEnum.LIMIT_AND_OFFSET + }); + + function formatResult(result: Storage[]) { + return result.map((e: any) => { + return { + id: e.storage_id, + name: e.storage_name, + extName: e.storage_ext_name, + path: e.storage_path, + type: e.storage_type, + size: e.storage_size, + createdAt: e.storage_created_at, + username: e.user_username, + bussinessRecordId:e.storage_bussiness_record_id, + bussinessModule:e.storage_bussiness_module + }; + }); + } + + return { + items: formatResult(items), + ...rest + }; + } + + async count(): Promise { + return this.storageRepository.count(); + } +} diff --git a/src/modules/tools/tools.module.ts b/src/modules/tools/tools.module.ts new file mode 100644 index 0000000..efbd337 --- /dev/null +++ b/src/modules/tools/tools.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; + +import { RouterModule } from '@nestjs/core'; + +import { EmailModule } from './email/email.module'; +import { StorageModule } from './storage/storage.module'; +import { UploadModule } from './upload/upload.module'; + +const modules = [StorageModule, EmailModule, UploadModule]; + +@Module({ + imports: [ + ...modules, + RouterModule.register([ + { + path: 'tools', + module: ToolsModule, + children: [...modules] + } + ]) + ], + exports: [...modules] +}) +export class ToolsModule {} diff --git a/src/modules/tools/upload/file.constraint.ts b/src/modules/tools/upload/file.constraint.ts new file mode 100644 index 0000000..ab3caf2 --- /dev/null +++ b/src/modules/tools/upload/file.constraint.ts @@ -0,0 +1,51 @@ +import { FastifyMultipartBaseOptions, MultipartFile } from '@fastify/multipart'; +import { + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, + registerDecorator +} from 'class-validator'; +import { has, isArray } from 'lodash'; + +type FileLimit = Pick & { + mimetypes?: string[]; +}; +function checkFileAndLimit(file: MultipartFile, limits: FileLimit = {}) { + if (!('mimetype' in file)) return false; + if (limits.mimetypes && !limits.mimetypes.includes(file.mimetype)) return false; + if (has(file, '_buf') && Buffer.byteLength((file as any)._buf) > limits.fileSize) return false; + return true; +} + +@ValidatorConstraint({ name: 'isFile' }) +export class FileConstraint implements ValidatorConstraintInterface { + validate(value: MultipartFile, args: ValidationArguments) { + const [limits = {}] = args.constraints; + const values = (args.object as any)[args.property]; + const filesLimit = (limits as FileLimit).files ?? 0; + if (filesLimit > 0 && isArray(values) && values.length > filesLimit) return false; + return checkFileAndLimit(value, limits); + } + + defaultMessage(_args: ValidationArguments) { + return `The file which to upload's conditions are not met`; + } +} + +/** + * 图片验证规则 + * @param limits 限制选项 + * @param validationOptions class-validator选项 + */ +export function IsFile(limits?: FileLimit, validationOptions?: ValidationOptions) { + return (object: Record, propertyName: string) => { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [limits], + validator: FileConstraint + }); + }; +} diff --git a/src/modules/tools/upload/upload.controller.ts b/src/modules/tools/upload/upload.controller.ts new file mode 100644 index 0000000..c01c713 --- /dev/null +++ b/src/modules/tools/upload/upload.controller.ts @@ -0,0 +1,51 @@ +import { BadRequestException, Body, Controller, Post, Req, UseInterceptors } from '@nestjs/common'; +import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { FastifyRequest } from 'fastify'; + +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { AuthUser } from '~/modules/auth/decorators/auth-user.decorator'; + +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; + +import { FileUploadDto } from './upload.dto'; +import { UploadService } from './upload.service'; + +export const permissions = definePermission('upload', { + UPLOAD: 'upload' +} as const); + +@ApiSecurityAuth() +@ApiTags('Tools - 上传模块') +@Controller('upload') +export class UploadController { + constructor(private uploadService: UploadService) {} + + @Post() + @Perm(permissions.UPLOAD) + @ApiOperation({ summary: '上传' }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + type: FileUploadDto + }) + async upload(@Req() req: FastifyRequest, @AuthUser() user: IAuthUser, @Body() body: any) { + if (!req.isMultipart()) throw new BadRequestException('Request is not multipart'); + const { file } = body; + const bussinessModule = body.bussinessModule?.value; + const bussinessRecordId = Number(body.bussinessRecordId?.value) || null; + try { + const savedFile = await this.uploadService.saveFile( + file, + user.uid, + bussinessModule, + bussinessRecordId + ); + + return { + filename: savedFile + }; + } catch (error) { + console.log(error); + throw new BadRequestException('上传失败'); + } + } +} diff --git a/src/modules/tools/upload/upload.dto.ts b/src/modules/tools/upload/upload.dto.ts new file mode 100644 index 0000000..05f25b0 --- /dev/null +++ b/src/modules/tools/upload/upload.dto.ts @@ -0,0 +1,22 @@ +import { MultipartFile } from '@fastify/multipart'; +import { ApiProperty } from '@nestjs/swagger'; + +import { IsDefined, IsNumber, IsOptional, IsString } from 'class-validator'; + +import { IsFile } from './file.constraint'; + +export class FileUploadDto { + @ApiProperty({ type: Buffer, format: 'binary', description: '文件' }) + @IsDefined() + // @IsFile(// + // { + // mimetypes: ['image/png', 'image/gif', 'image/jpeg', 'image/webp', 'image/svg+xml','apk'], + // // 改成30m + // fileSize: 30 * 1024 * 1024 * 1024 * 1024// 30MB + // }, + // { + // message: '文件类型不正确' + // } + // ) + file: MultipartFile; +} diff --git a/src/modules/tools/upload/upload.module.ts b/src/modules/tools/upload/upload.module.ts new file mode 100644 index 0000000..4f14cc1 --- /dev/null +++ b/src/modules/tools/upload/upload.module.ts @@ -0,0 +1,16 @@ +import { Module, forwardRef } from '@nestjs/common'; + +import { StorageModule } from '../storage/storage.module'; + +import { UploadController } from './upload.controller'; +import { UploadService } from './upload.service'; + +const services = [UploadService]; + +@Module({ + imports: [forwardRef(() => StorageModule)], + controllers: [UploadController], + providers: [...services], + exports: [...services] +}) +export class UploadModule {} diff --git a/src/modules/tools/upload/upload.service.ts b/src/modules/tools/upload/upload.service.ts new file mode 100644 index 0000000..0d79f9c --- /dev/null +++ b/src/modules/tools/upload/upload.service.ts @@ -0,0 +1,60 @@ +import { MultipartFile } from '@fastify/multipart'; +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { isNil } from 'lodash'; +import { Repository } from 'typeorm'; + +import { Storage } from '~/modules/tools/storage/storage.entity'; + +import { + UploadFileType, + fileRename, + getExtname, + getFilePath, + getFileType, + getSize, + saveLocalFile +} from '~/utils/file.util'; + +@Injectable() +export class UploadService { + constructor( + @InjectRepository(Storage) + private storageRepository: Repository + ) {} + + /** + * 保存文件上传记录 + */ + async saveFile( + file: MultipartFile, + userId: number, + bussinessModule?: string, + bussinessRecordId?: number + ): Promise<{ id: number; path: string }> { + if (isNil(file)) throw new NotFoundException('Have not any file to upload!'); + + const fileName = file.filename; + const size = getSize(file.file.bytesRead); + const extName = getExtname(fileName); + const type = getFileType(extName); + const name = type !== UploadFileType.APK ? fileRename(fileName) : fileName; + const path = getFilePath(name); + + saveLocalFile(await file.toBuffer(), name); + + const storage = await this.storageRepository.save({ + name, + fileName, + extName, + path, + type, + size, + userId, + bussinessModule, + bussinessRecordId + }); + + return { path, id: storage.id }; + } +} diff --git a/src/modules/user/constant.ts b/src/modules/user/constant.ts new file mode 100644 index 0000000..82e2c34 --- /dev/null +++ b/src/modules/user/constant.ts @@ -0,0 +1,4 @@ +export enum UserStatus { + Disable = 0, + Enabled = 1 +} diff --git a/src/modules/user/dto/password.dto.ts b/src/modules/user/dto/password.dto.ts new file mode 100644 index 0000000..ea5fedb --- /dev/null +++ b/src/modules/user/dto/password.dto.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, Matches, MaxLength, MinLength } from 'class-validator'; + +export class PasswordUpdateDto { + @ApiProperty({ description: '旧密码' }) + @IsString() + @Matches(/^[a-z0-9A-Z\W_]+$/) + @MinLength(6) + @MaxLength(20) + oldPassword: string; + + @ApiProperty({ description: '新密码' }) + @Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/, { + message: '密码必须包含数字、字母,长度为6-16' + }) + newPassword: string; +} + +export class UserPasswordDto { + // @ApiProperty({ description: '管理员/用户ID' }) + // @IsEntityExist(UserEntity, { message: '用户不存在' }) + // @IsInt() + // id: number + + @ApiProperty({ description: '更改后的密码' }) + @Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/, { + message: '密码格式不正确' + }) + password: string; +} + +export class UserExistDto { + @ApiProperty({ description: '登录账号' }) + @IsString() + @Matches(/^[a-zA-Z0-9_-]{4,16}$/) + @MinLength(6) + @MaxLength(20) + username: string; +} diff --git a/src/modules/user/dto/user.dto.ts b/src/modules/user/dto/user.dto.ts new file mode 100644 index 0000000..24193b5 --- /dev/null +++ b/src/modules/user/dto/user.dto.ts @@ -0,0 +1,104 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + ArrayMaxSize, + ArrayMinSize, + ArrayNotEmpty, + IsEmail, + IsIn, + IsInt, + IsOptional, + IsString, + Matches, + MaxLength, + MinLength, + ValidateIf +} from 'class-validator'; +import { isEmpty } from 'lodash'; +import { DomainType } from '~/common/decorators/domain.decorator'; + +import { PagerDto } from '~/common/dto/pager.dto'; + +export class UserDto extends DomainType { + @ApiProperty({ description: '头像' }) + @IsOptional() + @IsString() + avatar?: string; + + @ApiProperty({ description: '登录账号', example: 'admin' }) + @IsString() + @Matches(/^[a-z0-9A-Z\W_]+$/) + @MinLength(1) + @MaxLength(20) + username: string; + + @ApiProperty({ description: '登录密码', example: 'a123456' }) + @IsOptional() + @Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/, { + message: '密码必须包含数字、字母,长度为6-16' + }) + password: string; + + @ApiProperty({ description: '归属角色', type: [Number] }) + @ArrayNotEmpty() + @ArrayMinSize(1) + @ArrayMaxSize(3) + roleIds: number[]; + + @ApiProperty({ description: '归属大区', type: Number }) + @Type(() => Number) + @IsInt() + @IsOptional() + deptId?: number; + + @ApiProperty({ description: '呢称', example: 'admin' }) + @IsOptional() + @IsString() + nickname: string; + + @ApiProperty({ description: '邮箱', example: 'bqy.dev@qq.com' }) + @IsEmail() + @ValidateIf(o => !isEmpty(o.email)) + email: string; + + @ApiProperty({ description: '手机号' }) + @IsOptional() + @IsString() + phone?: string; + + @ApiProperty({ description: 'QQ' }) + @IsOptional() + @IsString() + @Matches(/^[1-9][0-9]{4,10}$/) + @MinLength(5) + @MaxLength(11) + qq?: string; + + @ApiProperty({ description: '备注' }) + @IsOptional() + @IsString() + remark?: string; + + @ApiProperty({ description: '状态' }) + @IsIn([0, 1]) + status: number; +} + +export class UserUpdateDto extends PartialType(UserDto) { } + +export class UserQueryDto extends IntersectionType(PagerDto, PartialType(UserDto), DomainType) { + @ApiProperty({ description: '归属大区', example: 1, required: false }) + @IsInt() + @IsOptional() + deptId?: number; + + @ApiProperty({ description: '状态', example: 0, required: false }) + @IsInt() + @IsOptional() + status?: number; + + @ApiProperty({ description: '关键字' }) + @IsString() + @IsOptional() + keyword?: string; +} diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts new file mode 100644 index 0000000..f09a413 --- /dev/null +++ b/src/modules/user/user.controller.ts @@ -0,0 +1,98 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + ParseArrayPipe, + Post, + Put, + Query +} from '@nestjs/common'; +import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; + +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { MenuService } from '~/modules/system/menu/menu.service'; + +import { Perm, definePermission } from '../auth/decorators/permission.decorator'; + +import { UserPasswordDto } from './dto/password.dto'; +import { UserDto, UserQueryDto, UserUpdateDto } from './dto/user.dto'; +import { UserEntity } from './user.entity'; +import { UserService } from './user.service'; +import { Domain, DomainType, SkDomain } from '~/common/decorators/domain.decorator'; + +export const permissions = definePermission('system:user', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete', + + PASSWORD_UPDATE: 'password:update', + PASSWORD_RESET: 'pass:reset' +} as const); + +@ApiTags('System - 用户模块') +@ApiSecurityAuth() +@Controller('users') +export class UserController { + constructor( + private userService: UserService, + private menuService: MenuService + ) { } + + @Get() + @ApiOperation({ summary: '获取用户列表' }) + @ApiResult({ type: [UserEntity], isPage: true }) + @Perm(permissions.LIST) + async list(@Domain() domain: SkDomain, @Query() dto: UserQueryDto) { + return this.userService.list(dto, domain); + } + + @Get(':id') + @ApiOperation({ summary: '查询用户' }) + @Perm(permissions.READ) + async read(@IdParam() id: number) { + return this.userService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增用户' }) + @Perm(permissions.CREATE) + async create(@Domain() domain: SkDomain, @Body() dto: UserDto): Promise { + await this.userService.create({ ...dto, domain }); + } + + @Put(':id') + @ApiOperation({ summary: '更新用户' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: UserUpdateDto): Promise { + await this.userService.update(id, dto); + await this.menuService.refreshPerms(id); + } + + @Delete(':id') + @ApiOperation({ summary: '删除用户' }) + @ApiParam({ + name: 'id', + type: String, + schema: { oneOf: [{ type: 'string' }, { type: 'number' }] } + }) + @Perm(permissions.DELETE) + async delete( + @Param('id', new ParseArrayPipe({ items: Number, separator: ',' })) ids: number[] + ): Promise { + await this.userService.delete(ids); + await this.userService.multiForbidden(ids); + } + + @Post(':id/password') + @ApiOperation({ summary: '更改用户密码' }) + @Perm(permissions.PASSWORD_UPDATE) + async password(@IdParam() id: number, @Body() dto: UserPasswordDto): Promise { + await this.userService.forceUpdatePassword(id, dto.password); + } +} diff --git a/src/modules/user/user.entity.ts b/src/modules/user/user.entity.ts new file mode 100644 index 0000000..5069c63 --- /dev/null +++ b/src/modules/user/user.entity.ts @@ -0,0 +1,96 @@ +import { ApiHideProperty } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import pinyin from 'pinyin'; +import { + BeforeInsert, + BeforeUpdate, + Column, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, + Relation +} from 'typeorm'; +import { SkDomain } from '~/common/decorators/domain.decorator'; + +import { CommonEntity } from '~/common/entity/common.entity'; + +import { AccessTokenEntity } from '~/modules/auth/entities/access-token.entity'; + +import { DeptEntity } from '~/modules/system/dept/dept.entity'; +import { RoleEntity } from '~/modules/system/role/role.entity'; + +@Entity({ name: 'sys_user' }) +export class UserEntity extends CommonEntity { + @Column({ unique: true }) + username: string; + + @Exclude() + @Column() + password: string; + + @Column({ length: 32 }) + psalt: string; + + @Column({ nullable: true }) + nickname: string; + + @ApiHideProperty() + @Column({ + name: 'name_pinyin', + type: 'varchar', + length: 255, + nullable: true, + comment: '产品名称的拼音' + }) + namePinyin: string; + + @BeforeInsert() + @BeforeUpdate() + updateNamePinyin() { + this.namePinyin = pinyin(this.nickname, { + style: pinyin.STYLE_NORMAL, + heteronym: false + }).join(''); + } + + @Column({ name: 'avatar', nullable: true }) + avatar: string; + + @Column({ nullable: true }) + qq: string; + + @Column({ nullable: true }) + email: string; + + @Column({ nullable: true }) + phone: string; + + @Column({ nullable: true }) + remark: string; + + @Column({ type: 'tinyint', nullable: true, default: 1 }) + status: number; + + @Column({ type: 'int', default: 1, comment: '所属域' }) + domain: SkDomain; + + @ManyToMany(() => RoleEntity, role => role.users) + @JoinTable({ + name: 'sys_user_roles', + joinColumn: { name: 'user_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'role_id', referencedColumnName: 'id' } + }) + roles: Relation; + + @ManyToOne(() => DeptEntity, dept => dept.users) + @JoinColumn({ name: 'dept_id' }) + dept: Relation; + + @OneToMany(() => AccessTokenEntity, accessToken => accessToken.user, { + cascade: true + }) + accessTokens: Relation; +} diff --git a/src/modules/user/user.model.ts b/src/modules/user/user.model.ts new file mode 100644 index 0000000..beb082b --- /dev/null +++ b/src/modules/user/user.model.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class AccountInfo { + @ApiProperty({ description: '用户名' }) + username: string; + + @ApiProperty({ description: '昵称' }) + nickname: string; + + @ApiProperty({ description: '邮箱' }) + email: string; + + @ApiProperty({ description: '手机号' }) + phone: string; + + @ApiProperty({ description: '备注' }) + remark: string; + + @ApiProperty({ description: '头像' }) + avatar: string; +} diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts new file mode 100644 index 0000000..621db3a --- /dev/null +++ b/src/modules/user/user.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { MenuModule } from '../system/menu/menu.module'; +import { ParamConfigModule } from '../system/param-config/param-config.module'; + +import { RoleModule } from '../system/role/role.module'; + +import { UserController } from './user.controller'; +import { UserEntity } from './user.entity'; +import { UserService } from './user.service'; + +const providers = [UserService]; + +@Module({ + imports: [TypeOrmModule.forFeature([UserEntity]), RoleModule, MenuModule, ParamConfigModule], + controllers: [UserController], + providers: [...providers], + exports: [TypeOrmModule, ...providers] +}) +export class UserModule {} diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts new file mode 100644 index 0000000..9be9d0f --- /dev/null +++ b/src/modules/user/user.service.ts @@ -0,0 +1,359 @@ +import { InjectRedis } from '@liaoliaots/nestjs-redis'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import Redis from 'ioredis'; +import { isEmpty, isNil, isNumber } from 'lodash'; + +import { EntityManager, In, Like, Repository } from 'typeorm'; +import pinyin from 'pinyin'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { ROOT_ROLE_ID, SYS_USER_INITPASSWORD } from '~/constants/system.constant'; +import { genAuthPVKey, genAuthPermKey, genAuthTokenKey } from '~/helper/genRedisKey'; + +import { paginate } from '~/helper/paginate'; +import { Pagination } from '~/helper/paginate/pagination'; +import { AccountUpdateDto } from '~/modules/auth/dto/account.dto'; +import { RegisterDto } from '~/modules/auth/dto/auth.dto'; +import { QQService } from '~/shared/helper/qq.service'; + +import { md5, randomValue } from '~/utils'; + +import { DeptEntity } from '../system/dept/dept.entity'; +import { ParamConfigService } from '../system/param-config/param-config.service'; +import { RoleEntity } from '../system/role/role.entity'; + +import { UserStatus } from './constant'; +import { PasswordUpdateDto } from './dto/password.dto'; +import { UserDto, UserQueryDto, UserUpdateDto } from './dto/user.dto'; +import { UserEntity } from './user.entity'; +import { AccountInfo } from './user.model'; +import { SkDomain } from '~/common/decorators/domain.decorator'; + +@Injectable() +export class UserService { + constructor( + @InjectRedis() + private readonly redis: Redis, + @InjectRepository(UserEntity) + private readonly userRepository: Repository, + @InjectRepository(RoleEntity) + private readonly roleRepository: Repository, + @InjectEntityManager() private entityManager: EntityManager, + private readonly paramConfigService: ParamConfigService, + private readonly qqService: QQService + ) {} + + async findUserById(id: number): Promise { + return this.userRepository + .createQueryBuilder('user') + .where({ + id, + status: UserStatus.Enabled + }) + .getOne(); + } + + async findUserByUserName(username: string): Promise { + return this.userRepository + .createQueryBuilder('user') + .where({ + username, + status: UserStatus.Enabled + }) + .getOne(); + } + + /** + * 获取用户信息 + * @param uid user id + */ + async getAccountInfo(uid: number): Promise { + const user: UserEntity = await this.userRepository + .createQueryBuilder('user') + .leftJoinAndSelect('user.roles', 'role') + .leftJoinAndSelect('user.dept', 'dept') + .where(`user.id = :uid`, { uid }) + .getOne(); + + if (isEmpty(user)) throw new BusinessException(ErrorEnum.USER_NOT_FOUND); + + delete user?.psalt; + + return user; + } + + /** + * 更新个人信息 + */ + async updateAccountInfo(uid: number, info: AccountUpdateDto): Promise { + const user = await this.userRepository.findOneBy({ id: uid }); + if (isEmpty(user)) throw new BusinessException(ErrorEnum.USER_NOT_FOUND); + + const data = { + ...(info.nickname ? { nickname: info.nickname } : null), + ...(info.avatar ? { avatar: info.avatar } : null), + ...(info.email ? { email: info.email } : null), + ...(info.phone ? { phone: info.phone } : null), + ...(info.qq ? { qq: info.qq } : null), + ...(info.remark ? { remark: info.remark } : null) + }; + + if (!info.avatar && info.qq) { + // 如果qq不等于原qq,则更新qq头像 + if (info.qq !== user.qq) data.avatar = await this.qqService.getAvater(info.qq); + } + + await this.userRepository.update(uid, data); + } + + /** + * 更改密码 + */ + async updatePassword(uid: number, dto: PasswordUpdateDto): Promise { + const user = await this.userRepository.findOneBy({ id: uid }); + if (isEmpty(user)) throw new BusinessException(ErrorEnum.USER_NOT_FOUND); + + const comparePassword = md5(`${dto.oldPassword}${user.psalt}`); + // 原密码不一致,不允许更改 + if (user.password !== comparePassword) throw new BusinessException(ErrorEnum.PASSWORD_MISMATCH); + + const password = md5(`${dto.newPassword}${user.psalt}`); + await this.userRepository.update({ id: uid }, { password }); + await this.upgradePasswordV(user.id); + } + + /** + * 直接更改密码 + */ + async forceUpdatePassword(uid: number, password: string): Promise { + const user = await this.userRepository.findOneBy({ id: uid }); + + const newPassword = md5(`${password}${user.psalt}`); + await this.userRepository.update({ id: uid }, { password: newPassword }); + await this.upgradePasswordV(user.id); + } + + /** + * 增加系统用户,如果返回false则表示已存在该用户 + */ + async create({ username, password, roleIds, deptId, ...data }: UserDto): Promise { + const exists = await this.userRepository.findOneBy({ + username + }); + if (!isEmpty(exists)) throw new BusinessException(ErrorEnum.SYSTEM_USER_EXISTS); + + await this.entityManager.transaction(async manager => { + const salt = randomValue(32); + + if (!password) { + const initPassword = await this.paramConfigService.findValueByKey(SYS_USER_INITPASSWORD); + password = md5(`${initPassword ?? '123456'}${salt}`); + } else { + password = md5(`${password ?? '123456'}${salt}`); + } + const u = manager.create(UserEntity, { + username, + password, + ...data, + psalt: salt, + roles: await this.roleRepository.findBy({ id: In(roleIds) }), + dept: await DeptEntity.findOneBy({ id: deptId }) + }); + + const result = await manager.save(u); + return result; + }); + } + + /** + * 更新用户信息 + */ + async update( + id: number, + { password, deptId, roleIds, status, ...data }: UserUpdateDto + ): Promise { + await this.entityManager.transaction(async manager => { + if (password) await this.forceUpdatePassword(id, password); + + await manager.update(UserEntity, id, { + ...data, + status + }); + + const user = await this.userRepository + .createQueryBuilder('user') + .leftJoinAndSelect('user.roles', 'roles') + .leftJoinAndSelect('user.dept', 'dept') + .where('user.id = :id', { id }) + .getOne(); + + await manager + .createQueryBuilder() + .relation(UserEntity, 'roles') + .of(id) + .addAndRemove(roleIds, user.roles); + + await manager.createQueryBuilder().relation(UserEntity, 'dept').of(id).set(deptId); + + if (status === 0) { + // 禁用状态 + await this.forbidden(id); + } + }); + } + + /** + * 查找用户信息 + * @param id 用户id + */ + async info(id: number): Promise { + const user = await this.userRepository + .createQueryBuilder('user') + .leftJoinAndSelect('user.roles', 'roles') + .leftJoinAndSelect('user.dept', 'dept') + .where('user.id = :id', { id }) + .getOne(); + + delete user.password; + delete user.psalt; + + return user; + } + + /** + * 根据ID列表删除用户 + */ + async delete(userIds: number[]): Promise { + const rootUserId = await this.findRootUserId(); + if (userIds.includes(rootUserId)) throw new BadRequestException('不能删除root用户!'); + + await this.userRepository.delete(userIds); + } + + /** + * 查找超管的用户ID + */ + async findRootUserId(): Promise { + const user = await this.userRepository.findOneBy({ + roles: { id: ROOT_ROLE_ID } + }); + return user.id; + } + + /** + * 查询用户列表 + */ + async list( + { page, pageSize, username, nickname, deptId, email, status, keyword }: UserQueryDto, + domain: SkDomain + ): Promise> { + const queryBuilder = this.userRepository + .createQueryBuilder('user') + .leftJoinAndSelect('user.dept', 'dept') + .leftJoinAndSelect('user.roles', 'role') + // .where('user.id NOT IN (:...ids)', { ids: [rootUserId, uid] }) + .where({ + ...(username ? { username: Like(`%${username}%`) } : null), + ...(nickname ? { nickname: Like(`%${nickname}%`) } : null), + ...(email ? { email: Like(`%${email}%`) } : null), + ...(isNumber(status) ? { status } : null) + }); + + if (deptId) queryBuilder.andWhere('dept.id = :deptId', { deptId }); + if (keyword) { + //关键字模糊查询product的name,productNumber,productSpecification + queryBuilder.andWhere( + `(user.nickname like :keyword or user.namePinyin like :keyword + or dept.name like :keyword + or user.phone like :keyword + or user.email like :keyword + or user.qq like :keyword + or user.remark like :keyword + )`, + { + keyword: `%${keyword}%` + } + ); + } + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 禁用用户 + */ + async forbidden(uid: number): Promise { + await this.redis.del(genAuthPVKey(uid)); + await this.redis.del(genAuthTokenKey(uid)); + await this.redis.del(genAuthPermKey(uid)); + } + + /** + * 禁用多个用户 + */ + async multiForbidden(uids: number[]): Promise { + if (uids) { + const pvs: string[] = []; + const ts: string[] = []; + const ps: string[] = []; + uids.forEach(uid => { + pvs.push(genAuthPVKey(uid)); + ts.push(genAuthTokenKey(uid)); + ps.push(genAuthPermKey(uid)); + }); + await this.redis.del(pvs); + await this.redis.del(ts); + await this.redis.del(ps); + } + } + + /** + * 升级用户版本密码 + */ + async upgradePasswordV(id: number): Promise { + // admin:passwordVersion:${param.id} + const v = await this.redis.get(genAuthPVKey(id)); + if (!isEmpty(v)) await this.redis.set(genAuthPVKey(id), Number.parseInt(v) + 1); + } + + /** + * 判断用户名是否存在 + */ + async exist(username: string) { + const user = await this.userRepository.findOneBy({ username }); + if (isNil(user)) throw new BusinessException(ErrorEnum.SYSTEM_USER_EXISTS); + + return true; + } + + /** + * 注册 + */ + async register({ username, ...data }: RegisterDto, domain: SkDomain): Promise { + const exists = await this.userRepository.findOneBy({ + username + }); + if (!isEmpty(exists)) throw new BusinessException(ErrorEnum.SYSTEM_USER_EXISTS); + + await this.entityManager.transaction(async manager => { + const salt = randomValue(32); + + const password = md5(`${data.password ?? 'a123456'}${salt}`); + + const u = manager.create(UserEntity, { + username, + password, + status: 1, + psalt: salt, + domain + }); + + const user = await manager.save(u); + + return user; + }); + } +} diff --git a/src/modules/vehicle_usage/vehicle_usage.controller.ts b/src/modules/vehicle_usage/vehicle_usage.controller.ts new file mode 100644 index 0000000..59d5f92 --- /dev/null +++ b/src/modules/vehicle_usage/vehicle_usage.controller.ts @@ -0,0 +1,80 @@ +import { + Body, + Controller, + Get, + Query, + Put, + Delete, + Post, + BadRequestException +} from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Perm, definePermission } from '../auth/decorators/permission.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; + +import { VehicleUsageService } from './vehicle_usage.service'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { VehicleUsageQueryDto, VehicleUsageDto, VehicleUsageUpdateDto } from './vehicle_usage.dto'; +import { VehicleUsageEntity } from '../vehicle_usage/vehicle_usage.entity'; +export const permissions = definePermission('app:vehicle_usage', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('VehicleUsage - 车辆使用') +@ApiSecurityAuth() +@Controller('vehicle-usage') +export class VehicleUsageController { + constructor(private vehicleUsageService: VehicleUsageService) {} + + @Get() + @ApiOperation({ summary: '获取车辆使用列表' }) + @ApiResult({ type: [VehicleUsageEntity], isPage: true }) + @Perm(permissions.LIST) + async list(@Query() dto: VehicleUsageQueryDto) { + return this.vehicleUsageService.findAll(dto); + } + + @Get(':id') + @ApiOperation({ summary: '获取车辆使用信息' }) + @ApiResult({ type: VehicleUsageDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.vehicleUsageService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增车辆使用' }) + @Perm(permissions.CREATE) + async create(@Body() dto: VehicleUsageDto): Promise { + await this.vehicleUsageService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新车辆使用' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: VehicleUsageUpdateDto): Promise { + await this.vehicleUsageService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除车辆使用' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.vehicleUsageService.delete(id); + } + + @Put('unlink-attachments/:id') + @ApiOperation({ summary: '附件解除关联' }) + @Perm(permissions.UPDATE) + async unlinkAttachments( + @IdParam() id: number, + @Body() { fileIds }: VehicleUsageUpdateDto + ): Promise { + await this.vehicleUsageService.unlinkAttachments(id, fileIds); + } +} diff --git a/src/modules/vehicle_usage/vehicle_usage.dto.ts b/src/modules/vehicle_usage/vehicle_usage.dto.ts new file mode 100644 index 0000000..e210ffb --- /dev/null +++ b/src/modules/vehicle_usage/vehicle_usage.dto.ts @@ -0,0 +1,81 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { Storage } from '../tools/storage/storage.entity'; + +export class VehicleUsageDto { + @ApiProperty({ description: '年度' }) + @IsNumber() + year: number; + + @ApiProperty({ description: '外出使用的车辆名称(字典)' }) + @IsNumber() + vehicleId: number; + + @ApiProperty({ description: '申请人' }) + @IsString() + applicant: string; + + @ApiProperty({ description: '出行司机' }) + @IsOptional() + @IsString() + driver: string; + + @ApiProperty({ description: '随行人员' }) + @IsOptional() + @IsString() + partner?: string; + + @ApiProperty({ description: '当前车辆里程数(KM)' }) + @IsOptional() + @IsNumber() + currentMileage: number; + + @ApiProperty({ description: '预计出行开始时间' }) + @IsOptional() + expectedStartDate: Date; + + @ApiProperty({ description: '预计出行结束时间' }) + @IsOptional() + expectedEndDate: Date; + + @ApiProperty({ description: '使用事由' }) + @IsOptional() + purpose: string; + + @ApiProperty({ description: '实际回司时间' }) + @IsOptional() + actualReturnTime: Date; + + @ApiProperty({ description: '回城车辆里程数(KM)' }) + @IsOptional() + returnMileage: number; + + @ApiProperty({ description: '审核人' }) + @IsOptional() + reviewer: string; + + @ApiProperty({ description: '审核状态:0待审核,1同意,2.不同意(字典)' }) + @IsOptional() + status: number; + + @ApiProperty({ description: '备注' }) + @IsOptional() + remark: string; +} + +export class VehicleUsageUpdateDto extends PartialType(VehicleUsageDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} + +export class VehicleUsageQueryDto extends IntersectionType( + PagerDto, + PartialType(VehicleUsageDto) +) { + @ApiProperty({ description: '车辆名称或者车牌号' }) + @IsOptional() + vehicle?: string; +} diff --git a/src/modules/vehicle_usage/vehicle_usage.entity.ts b/src/modules/vehicle_usage/vehicle_usage.entity.ts new file mode 100644 index 0000000..6821433 --- /dev/null +++ b/src/modules/vehicle_usage/vehicle_usage.entity.ts @@ -0,0 +1,103 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, Relation } from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; +import { DictItemEntity } from '../system/dict-item/dict-item.entity'; +import { Storage } from '../tools/storage/storage.entity'; + +@Entity({ name: 'vehicle_usage' }) +export class VehicleUsageEntity extends CommonEntity { + @Column({ type: 'int', comment: '年度' }) + @ApiProperty({ description: '年度' }) + year: number; + + @Column({ name: 'vehicle_id', type: 'int', comment: '外出使用的车辆名称(字典)' }) + @ApiProperty({ description: '外出使用的车辆名称(字典)' }) + vehicleId: number; + + @Column({ + name: 'applicant', + type: 'varchar', + length: 50, + comment: '申请人' + }) + @ApiProperty({ description: '申请人' }) + applicant: string; + + @Column({ + name: 'driver', + type: 'varchar', + length: 50, + comment: '出行司机', + nullable: true + }) + @ApiProperty({ description: '出行司机' }) + driver: string; + + @Column({ + name: 'partner', + type: 'varchar', + length: 50, + comment: '随行人员', + nullable: true + }) + @ApiProperty({ description: '随行人员' }) + partner: string; + + @Column({ name: 'current_mileage', type: 'int', comment: '当前车辆里程数(KM)', nullable: true }) + @ApiProperty({ description: '当前车辆里程数(KM)' }) + currentMileage: number; + + @Column({ + name: 'expected_start_date', + type: 'date', + nullable: true, + comment: '预计出行开始时间' + }) + @ApiProperty({ description: '预计出行开始时间' }) + expectedStartDate: Date; + + @Column({ name: 'expected_end_date', type: 'date', nullable: true, comment: '预计出行结束时间' }) + @ApiProperty({ description: '预计出行结束时间' }) + expectedEndDate: Date; + + @Column({ name: 'purpose', type: 'varchar', length: 255, comment: '使用事由', nullable: true }) + @ApiProperty({ description: '使用事由' }) + purpose: string; + + @Column({ + name: 'actual_return_time', + type: 'date', + nullable: true, + comment: '实际回司时间' + }) + @ApiProperty({ description: '实际回司时间' }) + actualReturnTime: Date; + + @Column({ name: 'return_mileage', type: 'int', comment: '回城车辆里程数(KM)', nullable: true }) + @ApiProperty({ description: '回城车辆里程数(KM)' }) + returnMileage: number; + + @Column({ name: 'reviewer', type: 'varchar', length: 50, comment: '审核人', nullable: true }) + @ApiProperty({ description: '审核人' }) + reviewer: string; + + @Column({ name: 'status', type: 'tinyint', default: 0, comment: '审核状态(字典)' }) + @ApiProperty({ description: '审核状态:0待审核,1同意,2.不同意(字典)' }) + status: number; + + @Column({ name: 'remark', type: 'varchar', length: 255, comment: '备注', nullable: true }) + @ApiProperty({ description: '备注' }) + remark: string; + + @ManyToOne(() => DictItemEntity) + @JoinColumn({ name: 'vehicle_id' }) + vehicle: DictItemEntity; + + @ManyToMany(() => Storage, storage => storage.vehicleUsage) + @JoinTable({ + name: 'vehicle_usage_storage', + joinColumn: { name: 'vehicle_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } + }) + files: Relation; +} diff --git a/src/modules/vehicle_usage/vehicle_usage.module.ts b/src/modules/vehicle_usage/vehicle_usage.module.ts new file mode 100644 index 0000000..e446f1e --- /dev/null +++ b/src/modules/vehicle_usage/vehicle_usage.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { VehicleUsageService } from './vehicle_usage.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { VehicleUsageEntity } from './vehicle_usage.entity'; +import { VehicleUsageController } from './vehicle_usage.controller'; +import { StorageModule } from '../tools/storage/storage.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([VehicleUsageEntity]), StorageModule], + providers: [VehicleUsageService], + controllers: [VehicleUsageController] +}) +export class VehicleUsageModule {} diff --git a/src/modules/vehicle_usage/vehicle_usage.service.ts b/src/modules/vehicle_usage/vehicle_usage.service.ts new file mode 100644 index 0000000..b1516d2 --- /dev/null +++ b/src/modules/vehicle_usage/vehicle_usage.service.ts @@ -0,0 +1,146 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, Repository, SelectQueryBuilder } from 'typeorm'; +import { VehicleUsageEntity } from './vehicle_usage.entity'; +import { Storage } from '../tools/storage/storage.entity'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { paginate } from '~/helper/paginate'; +import { Pagination } from '~/helper/paginate/pagination'; +import { fieldSearch } from '~/shared/database/field-search'; +import { VehicleUsageQueryDto, VehicleUsageDto } from './vehicle_usage.dto'; +import { VehicleUsageUpdateDto } from './vehicle_usage.dto'; + +@Injectable() +export class VehicleUsageService { + constructor( + @InjectRepository(VehicleUsageEntity) + private vehicleUsageRepository: Repository, + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(Storage) + private storageRepository: Repository + ) {} + + /** + * 分页查询所有 + */ + async findAll({ + page, + pageSize, + + ...fields + }: VehicleUsageQueryDto): Promise> { + const { vehicle, ...ext } = fields; + const sqb = this.buildLeftJoinRelations().where(fieldSearch(ext)); + if (vehicle) { + sqb.andWhere('vehicle.label like :vehicleName', { vehicleName: `%${vehicle}%` }); + } + return paginate(sqb, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create(dto: VehicleUsageDto): Promise { + // const { name, companyId } = dto; + // const isExsit = await this.vehicleUsageRepository.findOne({ + // where: { name, company: { id: companyId } } + // }); + // if (isExsit) { + // throw new BusinessException(ErrorEnum.PRODUCT_EXIST); + // } + await this.vehicleUsageRepository.insert(this.vehicleUsageRepository.create(dto)); + } + + /** + * 更新 + */ + async update(id: number, { fileIds, ...data }: Partial): Promise { + await this.entityManager.transaction(async manager => { + await manager.update( + VehicleUsageEntity, + id, + this.vehicleUsageRepository.create({ + ...data + }) + ); + const vehicleUsage = await this.vehicleUsageRepository + .createQueryBuilder('vehicle_usage') + .leftJoinAndSelect('vehicle_usage.files', 'files') + .where('vehicle_usage.id = :id', { id }) + .getOne(); + if (fileIds?.length) { + const count = await this.storageRepository + .createQueryBuilder('storage') + .where('storage.id in(:fileIds)', { fileIds }) + .getCount(); + if (count !== fileIds?.length) { + throw new BusinessException(ErrorEnum.STORAGE_NOT_FOUND); + } + // 附件要批量插入 + await manager + .createQueryBuilder() + .relation(VehicleUsageEntity, 'files') + .of(id) + .add(fileIds); + } + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + await this.vehicleUsageRepository.delete(id); + } + + /** + * 获取单个信息 + */ + async info(id: number) { + const info = this.buildLeftJoinRelations() + .where({ + id + }) + .getOne(); + return info; + } + + /** + * 解除附件关联 + * @param id 记录ID + * @param fileIds 附件ID + */ + async unlinkAttachments(id: number, fileIds: number[]) { + await this.entityManager.transaction(async manager => { + const vehicle_usage = await this.vehicleUsageRepository + .createQueryBuilder('vehicle_usage') + .leftJoinAndSelect('vehicle_usage.files', 'files') + .where('vehicle_usage.id = :id', { id }) + .getOne(); + const linkedFiles = vehicle_usage.files + .map(item => item.id) + .filter(item => !fileIds.includes(item)); + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(VehicleUsageEntity, 'files') + .of(id) + .addAndRemove(linkedFiles, vehicle_usage.files); + }); + } + + /** + * 封装和查询关联关系 + */ + buildLeftJoinRelations() { + return this.vehicleUsageRepository + .createQueryBuilder('vehicle_usage') + .leftJoin('vehicle_usage.files', 'files') + .leftJoin('vehicle_usage.vehicle', 'vehicle') + .addSelect(['files.id', 'files.path', 'vehicle.id', 'vehicle.label']); + } +} diff --git a/src/repl.ts b/src/repl.ts new file mode 100644 index 0000000..2a2a177 --- /dev/null +++ b/src/repl.ts @@ -0,0 +1,11 @@ +import { repl } from '@nestjs/core'; + +import { AppModule } from './app.module'; + +async function bootstrap() { + const replServer = await repl(AppModule); + replServer.setupHistory('.nestjs_repl_history', err => { + if (err) console.error(err); + }); +} +bootstrap(); diff --git a/src/setup-swagger.ts b/src/setup-swagger.ts new file mode 100644 index 0000000..8b49b32 --- /dev/null +++ b/src/setup-swagger.ts @@ -0,0 +1,43 @@ +import { INestApplication, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; + +import { API_SECURITY_AUTH } from './common/decorators/swagger.decorator'; +import { CommonEntity } from './common/entity/common.entity'; +import { ResOp, TreeResult } from './common/model/response.model'; +import { ConfigKeyPaths, IAppConfig, ISwaggerConfig } from './config'; +import { Pagination } from './helper/paginate/pagination'; + +export function setupSwagger( + app: INestApplication, + configService: ConfigService +): void { + const { name, port } = configService.get('app')!; + const { enable, path } = configService.get('swagger')!; + + if (!enable) return; + + const documentBuilder = new DocumentBuilder() + .setTitle(name) + .setDescription(`${name} API document`) + .setVersion('1.0'); + + // auth security + documentBuilder.addSecurity(API_SECURITY_AUTH, { + description: 'Auth', + type: 'apiKey', + in: 'header', + name: 'Authorization' + }); + + const document = SwaggerModule.createDocument(app, documentBuilder.build(), { + ignoreGlobalPrefix: false, + extraModels: [CommonEntity, ResOp, Pagination, TreeResult] + }); + + SwaggerModule.setup(path, app, document); + + // started log + const logger = new Logger('SwaggerModule'); + logger.log(`Document running on http://127.0.0.1:${port}/${path}`); +} diff --git a/src/shared/database/constraints/entity-exist.constraint.ts b/src/shared/database/constraints/entity-exist.constraint.ts new file mode 100644 index 0000000..37702b1 --- /dev/null +++ b/src/shared/database/constraints/entity-exist.constraint.ts @@ -0,0 +1,82 @@ +import { Injectable } from '@nestjs/common'; +import { + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, + registerDecorator +} from 'class-validator'; +import { DataSource, ObjectType, Repository } from 'typeorm'; + +interface Condition { + entity: ObjectType; + // 如果没有指定字段则使用当前验证的属性作为查询依据 + field?: string; +} + +/** + * 查询某个字段的值是否在数据表中存在 + */ +@ValidatorConstraint({ name: 'entityItemExist', async: true }) +@Injectable() +export class EntityExistConstraint implements ValidatorConstraintInterface { + constructor(private dataSource: DataSource) {} + + async validate(value: string, args: ValidationArguments) { + let repo: Repository; + + if (!value) return true; + // 默认对比字段是id + let field = 'id'; + // 通过传入的 entity 获取其 repository + if ('entity' in args.constraints[0]) { + // 传入的是对象 可以指定对比字段 + field = args.constraints[0].field ?? 'id'; + repo = this.dataSource.getRepository(args.constraints[0].entity); + } else { + // 传入的是实体类 + repo = this.dataSource.getRepository(args.constraints[0]); + } + // 通过查询记录是否存在进行验证 + const item = await repo.findOne({ where: { [field]: value } }); + return !!item; + } + + defaultMessage(args: ValidationArguments) { + if (!args.constraints[0]) return 'Model not been specified!'; + + return `All instance of ${args.constraints[0].name} must been exists in databse!`; + } +} + +/** + * 数据存在性验证 + * @param params Entity类或验证条件对象 + * @param validationOptions + */ +function IsEntityExist( + entity: ObjectType, + validationOptions?: ValidationOptions +): (object: Record, propertyName: string) => void; + +function IsEntityExist( + condition: { entity: ObjectType; field?: string }, + validationOptions?: ValidationOptions +): (object: Record, propertyName: string) => void; + +function IsEntityExist( + condition: ObjectType | { entity: ObjectType; field?: string }, + validationOptions?: ValidationOptions +): (object: Record, propertyName: string) => void { + return (object: Record, propertyName: string) => { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [condition], + validator: EntityExistConstraint + }); + }; +} + +export { IsEntityExist }; diff --git a/src/shared/database/constraints/unique.constraint.ts b/src/shared/database/constraints/unique.constraint.ts new file mode 100644 index 0000000..39d4fec --- /dev/null +++ b/src/shared/database/constraints/unique.constraint.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@nestjs/common'; +import { + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, + registerDecorator +} from 'class-validator'; +import { isNil, merge } from 'lodash'; +import { DataSource, ObjectType } from 'typeorm'; + +interface Condition { + entity: ObjectType; + // 如果没有指定字段则使用当前验证的属性作为查询依据 + field?: string; +} + +/** + * 验证某个字段的唯一性 + */ +@ValidatorConstraint({ name: 'entityItemUnique', async: true }) +@Injectable() +export class UniqueConstraint implements ValidatorConstraintInterface { + constructor(private dataSource: DataSource) {} + + async validate(value: any, args: ValidationArguments) { + // 获取要验证的模型和字段 + const config: Omit = { + field: args.property + }; + const condition = ('entity' in args.constraints[0] + ? merge(config, args.constraints[0]) + : { + ...config, + entity: args.constraints[0] + }) as unknown as Required; + if (!condition.entity) return false; + try { + // 查询是否存在数据,如果已经存在则验证失败 + const repo = this.dataSource.getRepository(condition.entity); + return isNil( + await repo.findOne({ + where: { [condition.field]: value } + }) + ); + } catch (err) { + // 如果数据库操作异常则验证失败 + return false; + } + } + + defaultMessage(args: ValidationArguments) { + const { entity, property } = args.constraints[0]; + const queryProperty = property ?? args.property; + if (!(args.object as any).getManager) return 'getManager function not been found!'; + + if (!entity) return 'Model not been specified!'; + + return `${queryProperty} of ${entity.name} must been unique!`; + } +} + +/** + * 数据唯一性验证 + * @param params Entity类或验证条件对象 + * @param validationOptions + */ +function IsUnique( + entity: ObjectType, + validationOptions?: ValidationOptions +): (object: Record, propertyName: string) => void; + +function IsUnique( + condition: Condition, + validationOptions?: ValidationOptions +): (object: Record, propertyName: string) => void; + +function IsUnique(params: ObjectType | Condition, validationOptions?: ValidationOptions) { + return (object: Record, propertyName: string) => { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [params], + validator: UniqueConstraint + }); + }; +} + +export { IsUnique }; diff --git a/src/shared/database/database.module.ts b/src/shared/database/database.module.ts new file mode 100644 index 0000000..eeee523 --- /dev/null +++ b/src/shared/database/database.module.ts @@ -0,0 +1,50 @@ +import { Module } from '@nestjs/common'; + +import { ConfigService } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { DataSource, LoggerOptions } from 'typeorm'; + +import { ConfigKeyPaths, IDatabaseConfig } from '~/config'; + +import { env } from '~/global/env'; + +import { EntityExistConstraint } from './constraints/entity-exist.constraint'; +import { UniqueConstraint } from './constraints/unique.constraint'; +import { TypeORMLogger } from './typeorm-logger'; + +const providers = [EntityExistConstraint, UniqueConstraint]; + +@Module({ + imports: [ + TypeOrmModule.forRootAsync({ + inject: [ConfigService], + useFactory: (configService: ConfigService) => { + let loggerOptions: LoggerOptions = env('DB_LOGGING') as 'all'; + + try { + // 解析成 js 数组 ['error'] + loggerOptions = JSON.parse(loggerOptions); + } catch { + // ignore + } + + return { + ...configService.get('database'), + autoLoadEntities: true, + logging: loggerOptions, + logger: new TypeORMLogger(loggerOptions) + }; + }, + // dataSource receives the configured DataSourceOptions + // and returns a Promise. + dataSourceFactory: async options => { + const dataSource = await new DataSource(options).initialize(); + return dataSource; + } + }) + ], + providers, + exports: providers +}) +export class DatabaseModule {} diff --git a/src/shared/database/field-search/index.ts b/src/shared/database/field-search/index.ts new file mode 100644 index 0000000..d8bdc44 --- /dev/null +++ b/src/shared/database/field-search/index.ts @@ -0,0 +1,38 @@ +import { isNumber } from 'lodash'; +import { Between, Like, ObjectLiteral } from 'typeorm'; +import { SkDomain } from '~/common/decorators/domain.decorator'; +export const fieldSearch = (entity: Partial): ObjectLiteral => { + let result = {}; + for (let key in entity) { + if (key == 'domain') { + result = { ...result, domain: entity['domain'] || -1 }; + } else if (entity.hasOwnProperty(key)) { + switch (typeof entity[key]) { + case 'number': + result = { ...result, ...(isNumber(entity[key]) ? { [key]: entity[key] } : null) }; + break; + case 'string': + result = { ...result, ...(entity[key] ? { [key]: Like(`%${entity[key]}%`) } : null) }; + break; + case 'boolean': + result = { ...result, ...(entity[key] === true ? { [key]: 1 } : { [key]: 0 }) }; + break; + case 'object': + if (Array.isArray(entity[key])) { + result = { + ...result, + ...(entity[key] && { [key]: Between(entity[key][0], entity[key][1]) }) + }; + } else { + // Handle other object types + } + break; + + default: + result = { ...result, ...(entity[key] ? { [key]: entity[key] } : null) }; + break; + } + } + } + return result; +}; diff --git a/src/shared/database/typeorm-logger.ts b/src/shared/database/typeorm-logger.ts new file mode 100644 index 0000000..8f425f7 --- /dev/null +++ b/src/shared/database/typeorm-logger.ts @@ -0,0 +1,103 @@ +import { Logger } from '@nestjs/common'; +import { Logger as ITypeORMLogger, LoggerOptions, QueryRunner } from 'typeorm'; + +export class TypeORMLogger implements ITypeORMLogger { + private logger = new Logger(TypeORMLogger.name); + + constructor(private options: LoggerOptions) {} + + logQuery(query: string, parameters?: any[], _queryRunner?: QueryRunner) { + if (!this.isEnable('query')) return; + + const sql = + query + + (parameters && parameters.length + ? ` -- PARAMETERS: ${this.stringifyParams(parameters)}` + : ''); + + this.logger.log(`[QUERY]: ${sql}`); + } + + logQueryError( + error: string | Error, + query: string, + parameters?: any[], + _queryRunner?: QueryRunner + ) { + if (!this.isEnable('error')) return; + + const sql = + query + + (parameters && parameters.length + ? ` -- PARAMETERS: ${this.stringifyParams(parameters)}` + : ''); + + this.logger.error([`[FAILED QUERY]: ${sql}`, `[QUERY ERROR]: ${error}`]); + } + + logQuerySlow(time: number, query: string, parameters?: any[], _queryRunner?: QueryRunner) { + const sql = + query + + (parameters && parameters.length + ? ` -- PARAMETERS: ${this.stringifyParams(parameters)}` + : ''); + + this.logger.warn(`[SLOW QUERY: ${time} ms]: ${sql}`); + } + + logSchemaBuild(message: string, _queryRunner?: QueryRunner) { + if (!this.isEnable('schema')) return; + + this.logger.log(message); + } + + logMigration(message: string, _queryRunner?: QueryRunner) { + if (!this.isEnable('migration')) return; + + this.logger.log(message); + } + + log(level: 'warn' | 'info' | 'log', message: any, _queryRunner?: QueryRunner) { + if (!this.isEnable(level)) return; + + switch (level) { + case 'log': + this.logger.debug(message); + break; + case 'info': + this.logger.log(message); + break; + case 'warn': + this.logger.warn(message); + break; + default: + break; + } + } + + /** + * Converts parameters to a string. + * Sometimes parameters can have circular objects and therefor we are handle this case too. + */ + private stringifyParams(parameters: any[]) { + try { + return JSON.stringify(parameters); + } catch (error) { + // most probably circular objects in parameters + return parameters; + } + } + + /** + * check enbale log + */ + private isEnable( + level: 'query' | 'schema' | 'error' | 'warn' | 'info' | 'log' | 'migration' + ): boolean { + return ( + this.options === 'all' || + this.options === true || + (Array.isArray(this.options) && this.options.includes(level)) + ); + } +} diff --git a/src/shared/helper/cron.service.ts b/src/shared/helper/cron.service.ts new file mode 100644 index 0000000..c53b4ea --- /dev/null +++ b/src/shared/helper/cron.service.ts @@ -0,0 +1,44 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { CronExpression } from '@nestjs/schedule'; +import dayjs from 'dayjs'; + +import { LessThan } from 'typeorm'; + +import { CronOnce } from '~/common/decorators/cron-once.decorator'; +import { ConfigKeyPaths } from '~/config'; +import { AccessTokenEntity } from '~/modules/auth/entities/access-token.entity'; + +@Injectable() +export class CronService { + private logger: Logger = new Logger(CronService.name); + constructor(private readonly configService: ConfigService) {} + + @CronOnce(CronExpression.EVERY_DAY_AT_MIDNIGHT) + async deleteExpiredJWT() { + this.logger.log('--> 开始扫表,清除过期的 token'); + + const expiredTokens = await AccessTokenEntity.find({ + where: { + expired_at: LessThan(new Date()) + } + }); + + let deleteCount = 0; + await Promise.all( + expiredTokens.map(async token => { + const { value, created_at } = token; + + await AccessTokenEntity.remove(token); + + this.logger.debug( + `--> 删除过期的 token:${value}, 签发于 ${dayjs(created_at).format('YYYY-MM-DD H:mm:ss')}` + ); + + deleteCount += 1; + }) + ); + + this.logger.log(`--> 删除了 ${deleteCount} 个过期的 token`); + } +} diff --git a/src/shared/helper/helper.module.ts b/src/shared/helper/helper.module.ts new file mode 100644 index 0000000..7ce4fc5 --- /dev/null +++ b/src/shared/helper/helper.module.ts @@ -0,0 +1,14 @@ +import { Global, Module, type Provider } from '@nestjs/common'; + +import { CronService } from './cron.service'; +import { QQService } from './qq.service'; + +const providers: Provider[] = [CronService, QQService]; + +@Global() +@Module({ + imports: [], + providers, + exports: providers +}) +export class HelperModule {} diff --git a/src/shared/helper/qq.service.ts b/src/shared/helper/qq.service.ts new file mode 100644 index 0000000..568cef4 --- /dev/null +++ b/src/shared/helper/qq.service.ts @@ -0,0 +1,19 @@ +import { HttpService } from '@nestjs/axios'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class QQService { + constructor(private readonly http: HttpService) {} + + async getNickname(qq: string | number) { + const { data } = await this.http.axiosRef.get( + `https://users.qzone.qq.com/fcg-bin/cgi_get_portrait.fcg?uins=${qq}` + ); + return data; + } + + async getAvater(qq: string | number) { + // https://thirdqq.qlogo.cn/headimg_dl?dst_uin=1743369777&spec=640&img_type=jpg + return `https://thirdqq.qlogo.cn/g?b=qq&s=100&nk=${qq}`; + } +} diff --git a/src/shared/logger/logger.module.ts b/src/shared/logger/logger.module.ts new file mode 100644 index 0000000..385b778 --- /dev/null +++ b/src/shared/logger/logger.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; + +import { LoggerService } from './logger.service'; + +@Module({}) +export class LoggerModule { + static forRoot() { + return { + global: true, + module: LoggerModule, + providers: [LoggerService], + exports: [LoggerService] + }; + } +} diff --git a/src/shared/logger/logger.service.ts b/src/shared/logger/logger.service.ts new file mode 100644 index 0000000..bb9fd37 --- /dev/null +++ b/src/shared/logger/logger.service.ts @@ -0,0 +1,111 @@ +import { ConsoleLogger, ConsoleLoggerOptions, Injectable } from '@nestjs/common'; + +import { ConfigService } from '@nestjs/config'; +import type { Logger as WinstonLogger } from 'winston'; + +import { config, createLogger, format, transports } from 'winston'; + +import 'winston-daily-rotate-file'; + +import { ConfigKeyPaths } from '~/config'; + +export enum LogLevel { + ERROR = 'error', + WARN = 'warn', + INFO = 'info', + DEBUG = 'debug', + VERBOSE = 'verbose' +} + +@Injectable() +export class LoggerService extends ConsoleLogger { + private winstonLogger: WinstonLogger; + + constructor( + context: string, + options: ConsoleLoggerOptions, + private configService: ConfigService + ) { + super(context, options); + this.initWinston(); + } + + protected get level(): LogLevel { + return this.configService.get('app.logger.level', { infer: true }) as LogLevel; + } + + protected get maxFiles(): number { + return this.configService.get('app.logger.maxFiles', { infer: true }); + } + + protected initWinston(): void { + this.winstonLogger = createLogger({ + levels: config.npm.levels, + format: format.combine(format.errors({ stack: true }), format.timestamp(), format.json()), + transports: [ + new transports.DailyRotateFile({ + level: this.level, + filename: 'logs/app.%DATE%.log', + datePattern: 'YYYY-MM-DD', + maxFiles: this.maxFiles, + format: format.combine(format.timestamp(), format.json()), + auditFile: 'logs/.audit/app.json' + }), + new transports.DailyRotateFile({ + level: LogLevel.ERROR, + filename: 'logs/app-error.%DATE%.log', + datePattern: 'YYYY-MM-DD', + maxFiles: this.maxFiles, + format: format.combine(format.timestamp(), format.json()), + auditFile: 'logs/.audit/app-error.json' + }) + ] + }); + + // if (isDev) { + // this.winstonLogger.add( + // new transports.Console({ + // level: this.level, + // format: format.combine( + // format.simple(), + // format.colorize({ all: true }), + // ), + // }), + // ); + // } + } + + verbose(message: any, context?: string): void { + super.verbose.apply(this, [message, context]); + + this.winstonLogger.log(LogLevel.VERBOSE, message, { context }); + } + + debug(message: any, context?: string): void { + super.debug.apply(this, [message, context]); + + this.winstonLogger.log(LogLevel.DEBUG, message, { context }); + } + + log(message: any, context?: string): void { + super.log.apply(this, [message, context]); + + this.winstonLogger.log(LogLevel.INFO, message, { context }); + } + + warn(message: any, context?: string): void { + super.warn.apply(this, [message, context]); + + this.winstonLogger.log(LogLevel.WARN, message); + } + + error(message: any, stack?: string, context?: string): void { + super.error.apply(this, [message, stack, context]); + + const hasStack = !!context; + this.winstonLogger.log(LogLevel.ERROR, { + context: hasStack ? context : stack, + message: hasStack ? new Error(message) : message + }); + } +} diff --git a/src/shared/mailer/mailer.module.ts b/src/shared/mailer/mailer.module.ts new file mode 100644 index 0000000..25ba1d0 --- /dev/null +++ b/src/shared/mailer/mailer.module.ts @@ -0,0 +1,40 @@ +import { join } from 'node:path'; + +import { Module, Provider } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { MailerModule as NestMailerModule } from '@nestjs-modules/mailer'; +import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; + +import { ConfigKeyPaths, IAppConfig, IMailerConfig } from '~/config'; + +import { MailerService } from './mailer.service'; + +const providers: Provider[] = [MailerService]; + +@Module({ + imports: [ + NestMailerModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + transport: configService.get('mailer'), + defaults: { + from: { + name: configService.get('app').name, + address: configService.get('mailer').auth.user + } + }, + template: { + dir: join(__dirname, '..', '..', '/assets/templates'), + adapter: new HandlebarsAdapter(), + options: { + strict: true + } + } + }), + inject: [ConfigService] + }) + ], + providers, + exports: providers +}) +export class MailerModule {} diff --git a/src/shared/mailer/mailer.service.ts b/src/shared/mailer/mailer.service.ts new file mode 100644 index 0000000..cd6b90c --- /dev/null +++ b/src/shared/mailer/mailer.service.ts @@ -0,0 +1,128 @@ +import { InjectRedis } from '@liaoliaots/nestjs-redis'; +import { Inject, Injectable } from '@nestjs/common'; + +import { MailerService as NestMailerService } from '@nestjs-modules/mailer'; +import dayjs from 'dayjs'; + +import Redis from 'ioredis'; + +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { AppConfig, IAppConfig } from '~/config'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { randomValue } from '~/utils'; + +@Injectable() +export class MailerService { + constructor( + @Inject(AppConfig.KEY) private appConfig: IAppConfig, + @InjectRedis() private redis: Redis, + private mailerService: NestMailerService + ) {} + + async log(to: string, code: string, ip: string) { + const getRemainTime = () => { + const now = dayjs(); + return now.endOf('day').diff(now, 'second'); + }; + + await this.redis.set(`captcha:${to}`, code, 'EX', 60 * 5); + + const limitCountOfDay = await this.redis.get(`captcha:${to}:limit-day`); + const ipLimitCountOfDay = await this.redis.get(`ip:${ip}:send:limit-day`); + + await this.redis.set(`ip:${ip}:send:limit`, 1, 'EX', 60); + await this.redis.set(`captcha:${to}:limit`, 1, 'EX', 60); + await this.redis.set( + `captcha:${to}:send:limit-count-day`, + limitCountOfDay, + 'EX', + getRemainTime() + ); + await this.redis.set(`ip:${ip}:send:limit-count-day`, ipLimitCountOfDay, 'EX', getRemainTime()); + } + + async checkCode(to, code) { + const ret = await this.redis.get(`captcha:${to}`); + if (ret !== code) throw new BusinessException(ErrorEnum.INVALID_VERIFICATION_CODE); + + await this.redis.del(`captcha:${to}`); + } + + async checkLimit(to, ip) { + const LIMIT_TIME = 5; + + // ip限制 + const ipLimit = await this.redis.get(`ip:${ip}:send:limit`); + if (ipLimit) throw new BusinessException(ErrorEnum.TOO_MANY_REQUESTS); + + // 1分钟最多接收1条 + const limit = await this.redis.get(`captcha:${to}:limit`); + if (limit) throw new BusinessException(ErrorEnum.TOO_MANY_REQUESTS); + + // 1天一个邮箱最多接收5条 + let limitCountOfDay: string | number = await this.redis.get(`captcha:${to}:limit-day`); + limitCountOfDay = limitCountOfDay ? Number(limitCountOfDay) : 0; + if (limitCountOfDay > LIMIT_TIME) { + throw new BusinessException(ErrorEnum.MAXIMUM_FIVE_VERIFICATION_CODES_PER_DAY); + } + + // 1天一个ip最多发送5条 + let ipLimitCountOfDay: string | number = await this.redis.get(`ip:${ip}:send:limit-day`); + ipLimitCountOfDay = ipLimitCountOfDay ? Number(ipLimitCountOfDay) : 0; + if (ipLimitCountOfDay > LIMIT_TIME) { + throw new BusinessException(ErrorEnum.MAXIMUM_FIVE_VERIFICATION_CODES_PER_DAY); + } + } + + async send(to, subject, content: string, type: 'text' | 'html' = 'text'): Promise { + if (type === 'text') { + return this.mailerService.sendMail({ + to, + subject, + text: content + }); + } else { + return this.mailerService.sendMail({ + to, + subject, + html: content + }); + } + } + + async sendVerificationCode(to, code = randomValue(4, '1234567890')) { + const subject = `[${this.appConfig.name}] 验证码`; + + try { + await this.mailerService.sendMail({ + to, + subject, + template: './verification-code-zh', + context: { + code + } + }); + } catch (error) { + console.log(error); + throw new BusinessException(ErrorEnum.VERIFICATION_CODE_SEND_FAILED); + } + + return { + to, + code + }; + } + + // async sendUserConfirmation(user: UserEntity, token: string) { + // const url = `example.com/auth/confirm?token=${token}` + // await this.mailerService.sendMail({ + // to: user.email, + // subject: 'Confirm your Email', + // template: './confirmation', + // context: { + // name: user.name, + // url, + // }, + // }) + // } +} diff --git a/src/shared/redis/cache.service.ts b/src/shared/redis/cache.service.ts new file mode 100644 index 0000000..d8ccb54 --- /dev/null +++ b/src/shared/redis/cache.service.ts @@ -0,0 +1,67 @@ +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Inject, Injectable } from '@nestjs/common'; +import { Emitter } from '@socket.io/redis-emitter'; +import { Cache } from 'cache-manager'; +import type { Redis } from 'ioredis'; + +import { RedisIoAdapterKey } from '~/common/adapters/socket.adapter'; + +import { API_CACHE_PREFIX } from '~/constants/cache.constant'; +import { getRedisKey } from '~/utils/redis.util'; + +// 获取器 +export type TCacheKey = string; +export type TCacheResult = Promise; + +@Injectable() +export class CacheService { + private cache!: Cache; + + private ioRedis!: Redis; + constructor(@Inject(CACHE_MANAGER) cache: Cache) { + this.cache = cache; + } + + private get redisClient(): Redis { + // eslint-disable-next-line ts/ban-ts-comment + // @ts-expect-error + return this.cache.store.client; + } + + public get(key: TCacheKey): TCacheResult { + return this.cache.get(key); + } + + public set(key: TCacheKey, value: any, milliseconds: number) { + return this.cache.set(key, value, milliseconds); + } + + public getClient() { + return this.redisClient; + } + + private _emitter: Emitter; + + public get emitter(): Emitter { + if (this._emitter) return this._emitter; + + this._emitter = new Emitter(this.redisClient, { + key: RedisIoAdapterKey + }); + + return this._emitter; + } + + public async cleanCatch() { + const redis = this.getClient(); + const keys: string[] = await redis.keys(`${API_CACHE_PREFIX}*`); + await Promise.all(keys.map(key => redis.del(key))); + } + + public async cleanAllRedisKey() { + const redis = this.getClient(); + const keys: string[] = await redis.keys(getRedisKey('*')); + + await Promise.all(keys.map(key => redis.del(key))); + } +} diff --git a/src/shared/redis/redis-subpub.ts b/src/shared/redis/redis-subpub.ts new file mode 100644 index 0000000..3a8d07f --- /dev/null +++ b/src/shared/redis/redis-subpub.ts @@ -0,0 +1,65 @@ +import { Logger } from '@nestjs/common'; +import IORedis from 'ioredis'; +import type { Redis, RedisOptions } from 'ioredis'; + +export class RedisSubPub { + public pubClient: Redis; + public subClient: Redis; + constructor( + private redisConfig: RedisOptions, + private channelPrefix: string = 'm-shop-channel#' + ) { + this.init(); + } + + public init() { + const redisOptions: RedisOptions = { + host: this.redisConfig.host, + port: this.redisConfig.port + }; + + if (this.redisConfig.password) redisOptions.password = this.redisConfig.password; + + const pubClient = new IORedis(redisOptions); + const subClient = pubClient.duplicate(); + this.pubClient = pubClient; + this.subClient = subClient; + } + + public async publish(event: string, data: any) { + const channel = this.channelPrefix + event; + const _data = JSON.stringify(data); + if (event !== 'log') Logger.debug(`发布事件:${channel} <- ${_data}`, RedisSubPub.name); + + await this.pubClient.publish(channel, _data); + } + + private ctc = new WeakMap void>(); + + public async subscribe(event: string, callback: (data: any) => void) { + const myChannel = this.channelPrefix + event; + this.subClient.subscribe(myChannel); + + const cb = (channel, message) => { + if (channel === myChannel) { + if (event !== 'log') Logger.debug(`接收事件:${channel} -> ${message}`, RedisSubPub.name); + + callback(JSON.parse(message)); + } + }; + + this.ctc.set(callback, cb); + this.subClient.on('message', cb); + } + + public async unsubscribe(event: string, callback: (data: any) => void) { + const channel = this.channelPrefix + event; + this.subClient.unsubscribe(channel); + const cb = this.ctc.get(callback); + if (cb) { + this.subClient.off('message', cb); + + this.ctc.delete(callback); + } + } +} diff --git a/src/shared/redis/redis.constant.ts b/src/shared/redis/redis.constant.ts new file mode 100644 index 0000000..1111312 --- /dev/null +++ b/src/shared/redis/redis.constant.ts @@ -0,0 +1 @@ +export const REDIS_PUBSUB = Symbol('REDIS_PUBSUB'); diff --git a/src/shared/redis/redis.module.ts b/src/shared/redis/redis.module.ts new file mode 100644 index 0000000..8fa4c90 --- /dev/null +++ b/src/shared/redis/redis.module.ts @@ -0,0 +1,60 @@ +import { RedisModule as NestRedisModule } from '@liaoliaots/nestjs-redis'; +import { CacheModule } from '@nestjs/cache-manager'; +import { Global, Module, Provider } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; + +import { redisStore } from 'cache-manager-ioredis-yet'; +import { RedisOptions } from 'ioredis'; + +import { ConfigKeyPaths, IRedisConfig } from '~/config'; + +import { CacheService } from './cache.service'; +import { RedisSubPub } from './redis-subpub'; +import { REDIS_PUBSUB } from './redis.constant'; +import { RedisPubSubService } from './subpub.service'; + +const providers: Provider[] = [ + CacheService, + { + provide: REDIS_PUBSUB, + useFactory: (configService: ConfigService) => { + const redisOptions: RedisOptions = configService.get('redis'); + return new RedisSubPub(redisOptions); + }, + inject: [ConfigService] + }, + RedisPubSubService +]; + +@Global() +@Module({ + imports: [ + // cache + CacheModule.registerAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => { + const redisOptions: RedisOptions = configService.get('redis'); + + return { + isGlobal: true, + store: redisStore, + isCacheableValue: () => true, + ...redisOptions + }; + }, + inject: [ConfigService] + }), + // redis + NestRedisModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + readyLog: true, + config: configService.get('redis') + }), + inject: [ConfigService] + }) + ], + providers, + exports: [...providers, CacheModule] +}) +export class RedisModule {} diff --git a/src/shared/redis/subpub.service.ts b/src/shared/redis/subpub.service.ts new file mode 100644 index 0000000..fc9c409 --- /dev/null +++ b/src/shared/redis/subpub.service.ts @@ -0,0 +1,21 @@ +import { Inject, Injectable } from '@nestjs/common'; + +import { RedisSubPub } from './redis-subpub'; +import { REDIS_PUBSUB } from './redis.constant'; + +@Injectable() +export class RedisPubSubService { + constructor(@Inject(REDIS_PUBSUB) private readonly redisSubPub: RedisSubPub) {} + + public async publish(event: string, data: any) { + return this.redisSubPub.publish(event, data); + } + + public async subscribe(event: string, callback: (data: any) => void) { + return this.redisSubPub.subscribe(event, callback); + } + + public async unsubscribe(event: string, callback: (data: any) => void) { + return this.redisSubPub.unsubscribe(event, callback); + } +} diff --git a/src/shared/shared.module.ts b/src/shared/shared.module.ts new file mode 100644 index 0000000..5eaf868 --- /dev/null +++ b/src/shared/shared.module.ts @@ -0,0 +1,49 @@ +import { HttpModule } from '@nestjs/axios'; +import { Global, Module } from '@nestjs/common'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { ScheduleModule } from '@nestjs/schedule'; +import { ThrottlerModule } from '@nestjs/throttler'; + +import { isDev } from '~/global/env'; + +import { HelperModule } from './helper/helper.module'; +import { LoggerModule } from './logger/logger.module'; +import { MailerModule } from './mailer/mailer.module'; + +import { RedisModule } from './redis/redis.module'; + +@Global() +@Module({ + imports: [ + // logger + LoggerModule.forRoot(), + // http + HttpModule, + // schedule + ScheduleModule.forRoot(), + // rate limit + ThrottlerModule.forRoot([ + { + limit: 3, + ttl: 60000 + } + ]), + EventEmitterModule.forRoot({ + wildcard: true, + delimiter: '.', + newListener: false, + removeListener: false, + maxListeners: 20, + verboseMemoryLeak: isDev, + ignoreErrors: false + }), + // redis + RedisModule, + // mailer + MailerModule, + // helper + HelperModule + ], + exports: [HttpModule, MailerModule, RedisModule, HelperModule] +}) +export class SharedModule {} diff --git a/src/socket/base.gateway.ts b/src/socket/base.gateway.ts new file mode 100644 index 0000000..920e573 --- /dev/null +++ b/src/socket/base.gateway.ts @@ -0,0 +1,25 @@ +import type { Socket } from 'socket.io'; + +import { BusinessEvents } from './business-event.constant'; + +export abstract class BaseGateway { + public gatewayMessageFormat(type: BusinessEvents, message: any, code?: number) { + return { + type, + data: message, + code + }; + } + + handleDisconnect(client: Socket) { + client.send(this.gatewayMessageFormat(BusinessEvents.GATEWAY_CONNECT, 'WebSocket 断开')); + } + + handleConnect(client: Socket) { + client.send(this.gatewayMessageFormat(BusinessEvents.GATEWAY_CONNECT, 'WebSocket 已连接')); + } +} + +export abstract class BroadcastBaseGateway extends BaseGateway { + broadcast(event: BusinessEvents, data: any) {} +} diff --git a/src/socket/business-event.constant.ts b/src/socket/business-event.constant.ts new file mode 100644 index 0000000..5d0f133 --- /dev/null +++ b/src/socket/business-event.constant.ts @@ -0,0 +1,11 @@ +export enum BusinessEvents { + GATEWAY_CONNECT = 'GATEWAY_CONNECT', + GATEWAY_DISCONNECT = 'GATEWAY_DISCONNECT', + + AUTH_FAILED = 'AUTH_FAILED', + + // 用户上线事件 + USER_ONLINE = 'USER_ONLINE', + USER_OFFLINE = 'USER_OFFLINE', + USER_KICK = 'USER_KICK' +} diff --git a/src/socket/events/admin.gateway.ts b/src/socket/events/admin.gateway.ts new file mode 100644 index 0000000..26007b9 --- /dev/null +++ b/src/socket/events/admin.gateway.ts @@ -0,0 +1,38 @@ +import { JwtService } from '@nestjs/jwt'; +import { + GatewayMetadata, + OnGatewayConnection, + OnGatewayDisconnect, + WebSocketGateway, + WebSocketServer +} from '@nestjs/websockets'; + +import { Server } from 'socket.io'; + +import { AuthService } from '~/modules/auth/auth.service'; +import { CacheService } from '~/shared/redis/cache.service'; + +import { createAuthGateway } from '../shared/auth.gateway'; + +const AuthGateway = createAuthGateway({ namespace: 'admin' }); + +@WebSocketGateway({ namespace: 'admin' }) +export class AdminEventsGateway + extends AuthGateway + implements OnGatewayConnection, OnGatewayDisconnect +{ + constructor( + protected readonly jwtService: JwtService, + protected readonly authService: AuthService, + private readonly cacheService: CacheService + ) { + super(jwtService, authService, cacheService); + } + + @WebSocketServer() + protected _server: Server; + + get server() { + return this._server; + } +} diff --git a/src/socket/events/web.gateway.ts b/src/socket/events/web.gateway.ts new file mode 100644 index 0000000..c1250ae --- /dev/null +++ b/src/socket/events/web.gateway.ts @@ -0,0 +1,37 @@ +import { JwtService } from '@nestjs/jwt'; +import { + GatewayMetadata, + OnGatewayConnection, + OnGatewayDisconnect, + WebSocketGateway, + WebSocketServer +} from '@nestjs/websockets'; + +import { Server } from 'socket.io'; + +import { TokenService } from '~/modules/auth/services/token.service'; +import { CacheService } from '~/shared/redis/cache.service'; + +import { createAuthGateway } from '../shared/auth.gateway'; + +const AuthGateway = createAuthGateway({ namespace: 'web' }); +@WebSocketGateway({ namespace: 'web' }) +export class WebEventsGateway + extends AuthGateway + implements OnGatewayConnection, OnGatewayDisconnect +{ + constructor( + protected readonly jwtService: JwtService, + protected readonly tokenService: TokenService, + private readonly cacheService: CacheService + ) { + super(jwtService, tokenService, cacheService); + } + + @WebSocketServer() + protected _server: Server; + + get server() { + return this._server; + } +} diff --git a/src/socket/shared/auth.gateway.ts b/src/socket/shared/auth.gateway.ts new file mode 100644 index 0000000..c18e59d --- /dev/null +++ b/src/socket/shared/auth.gateway.ts @@ -0,0 +1,114 @@ +import {} from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { JwtService } from '@nestjs/jwt'; +import type { OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets'; +import { WebSocketServer } from '@nestjs/websockets'; +import { Namespace } from 'socket.io'; +import type { Socket } from 'socket.io'; + +import { EventBusEvents } from '~/constants/event-bus.constant'; + +import { TokenService } from '~/modules/auth/services/token.service'; +import { CacheService } from '~/shared/redis/cache.service'; + +import { BroadcastBaseGateway } from '../base.gateway'; +import { BusinessEvents } from '../business-event.constant'; + +export interface AuthGatewayOptions { + namespace: string; +} + +// eslint-disable-next-line ts/ban-ts-comment +// @ts-expect-error +export interface IAuthGateway + extends OnGatewayConnection, + OnGatewayDisconnect, + BroadcastBaseGateway {} + +export function createAuthGateway( + options: AuthGatewayOptions +): new (...args: any[]) => IAuthGateway { + const { namespace } = options; + + class AuthGateway extends BroadcastBaseGateway implements IAuthGateway { + constructor( + protected readonly jwtService: JwtService, + protected readonly tokenService: TokenService, + private readonly cacheService: CacheService + ) { + super(); + } + + @WebSocketServer() + protected namespace: Namespace; + + async authFailed(client: Socket) { + client.send(this.gatewayMessageFormat(BusinessEvents.AUTH_FAILED, '认证失败')); + client.disconnect(); + } + + async authToken(token: string): Promise { + if (typeof token !== 'string') return false; + + const validJwt = async () => { + try { + const ok = await this.jwtService.verify(token); + + if (!ok) return false; + } catch { + return false; + } + // is not crash, is verify + return true; + }; + + return await validJwt(); + } + + async handleConnection(client: Socket) { + const token = + client.handshake.query.token || + client.handshake.headers.authorization || + client.handshake.headers.Authorization; + if (!token) return this.authFailed(client); + + if (!(await this.authToken(token as string))) return this.authFailed(client); + + super.handleConnect(client); + + const sid = client.id; + this.tokenSocketIdMap.set(token.toString(), sid); + } + + handleDisconnect(client: Socket) { + super.handleDisconnect(client); + } + + tokenSocketIdMap = new Map(); + + @OnEvent(EventBusEvents.TokenExpired) + handleTokenExpired(token: string) { + // consola.debug(`token expired: ${token}`) + + const server = this.namespace.server; + const sid = this.tokenSocketIdMap.get(token); + if (!sid) return false; + + const socket = server.of(`/${namespace}`).sockets.get(sid); + if (socket) { + socket.disconnect(); + super.handleDisconnect(socket); + return true; + } + return false; + } + + override broadcast(event: BusinessEvents, data: any) { + this.cacheService.emitter + .of(`/${namespace}`) + .emit('message', this.gatewayMessageFormat(event, data)); + } + } + + return AuthGateway; +} diff --git a/src/socket/socket.module.ts b/src/socket/socket.module.ts new file mode 100644 index 0000000..adadb4b --- /dev/null +++ b/src/socket/socket.module.ts @@ -0,0 +1,16 @@ +import { Module, Provider, forwardRef } from '@nestjs/common'; + +import { AuthModule } from '../modules/auth/auth.module'; +import { SystemModule } from '../modules/system/system.module'; + +import { AdminEventsGateway } from './events/admin.gateway'; +import { WebEventsGateway } from './events/web.gateway'; + +const providers: Provider[] = [AdminEventsGateway, WebEventsGateway]; + +@Module({ + imports: [forwardRef(() => SystemModule), AuthModule], + providers, + exports: [...providers] +}) +export class SocketModule {} diff --git a/src/utils/captcha.util.ts b/src/utils/captcha.util.ts new file mode 100644 index 0000000..443b540 --- /dev/null +++ b/src/utils/captcha.util.ts @@ -0,0 +1,19 @@ +import svgCaptcha from 'svg-captcha'; + +export function createCaptcha() { + return svgCaptcha.createMathExpr({ + size: 4, + ignoreChars: '0o1iIl', + noise: 2, + color: true, + background: '#eee', + fontSize: 50, + width: 110, + height: 38 + }); +} + +export function createMathExpr() { + const options = {}; + return svgCaptcha.createMathExpr(options); +} diff --git a/src/utils/crypto.util.ts b/src/utils/crypto.util.ts new file mode 100644 index 0000000..1c072dd --- /dev/null +++ b/src/utils/crypto.util.ts @@ -0,0 +1,28 @@ +import CryptoJS from 'crypto-js'; + +const key = CryptoJS.enc.Utf8.parse('louisabcdefe9bc'); +const iv = CryptoJS.enc.Utf8.parse('0123456789louis'); + +export function aesEncrypt(data) { + if (!data) return data; + const enc = CryptoJS.AES.encrypt(data, key, { + iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7 + }); + return enc.toString(); +} + +export function aesDecrypt(data) { + if (!data) return data; + const dec = CryptoJS.AES.decrypt(data, key, { + iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7 + }); + return dec.toString(CryptoJS.enc.Utf8); +} + +export function md5(str: string) { + return CryptoJS.MD5(str).toString(); +} diff --git a/src/utils/date.util.ts b/src/utils/date.util.ts new file mode 100644 index 0000000..d275d1a --- /dev/null +++ b/src/utils/date.util.ts @@ -0,0 +1,23 @@ +import dayjs from 'dayjs'; +import { isDate } from 'lodash'; + +const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; +const DATE_FORMAT = 'YYYY-MM-DD'; + +export function formatToDateTime( + date: string | number | Date | dayjs.Dayjs | null | undefined = undefined, + format = DATE_TIME_FORMAT +): string { + return dayjs(date).format(format); +} + +export function formatToDate( + date: string | number | Date | dayjs.Dayjs | null | undefined = undefined, + format = DATE_FORMAT +): string { + return dayjs(date).format(format); +} + +export function isDateObject(obj: unknown): boolean { + return isDate(obj) || dayjs.isDayjs(obj); +} diff --git a/src/utils/file.util.ts b/src/utils/file.util.ts new file mode 100644 index 0000000..73fe482 --- /dev/null +++ b/src/utils/file.util.ts @@ -0,0 +1,85 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { MultipartFile } from '@fastify/multipart'; + +import dayjs from 'dayjs'; + +export enum UploadFileType { + IMAGE = '图片', + TXT = '文档', + MUSIC = '音乐', + VIDEO = '视频', + APK = 'apk', + OTHER = '其他' +} + +export function getFileType(extName: string) { + const documents = 'txt doc pdf ppt pps xlsx xls docx'; + const music = 'mp3 wav wma mpa ram ra aac aif m4a'; + const video = 'avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg'; + const image = 'bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg'; + const apk = 'apk'; + if (image.includes(extName)) return UploadFileType.IMAGE; + + if (documents.includes(extName)) return UploadFileType.TXT; + + if (music.includes(extName)) return UploadFileType.MUSIC; + + if (video.includes(extName)) return UploadFileType.VIDEO; + + if (apk.includes(extName)) return UploadFileType.APK; + return UploadFileType.OTHER; +} + +export function getName(fileName: string) { + if (fileName.includes('.')) return fileName.split('.')[0]; + + return fileName; +} + +export function getExtname(fileName: string) { + return path.extname(fileName).replace('.', ''); +} + +export function getSize(bytes: number, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; +} + +export function fileRename(fileName: string) { + const name = fileName.split('.')[0]; + const extName = path.extname(fileName); + const time = dayjs().format('YYYYMMDDHHmmSSS'); + return `${name}-${time}${extName}`; +} + +export function getFilePath(name: string) { + return `/upload/${name}`; +} + +export function saveLocalFile(buffer: Buffer, name: string) { + const filePath = path.join(__dirname, '../../', 'public/upload', name); + const writeStream = fs.createWriteStream(filePath); + writeStream.write(buffer); +} + +export async function saveFile(file: MultipartFile, name: string) { + const filePath = path.join(__dirname, '../../', 'public/upload', name); + const writeStream = fs.createWriteStream(filePath); + const buffer = await file.toBuffer(); + writeStream.write(buffer); +} + +export async function deleteFile(name: string) { + fs.unlink(path.join(__dirname, '../../', 'public', name), () => { + // console.log(error); + }); +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..598276a --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,11 @@ +export * from './captcha.util'; +export * from './crypto.util'; +export * from './date.util'; +export * from './file.util'; +export * from './ip.util'; +export * from './is.util'; +export * from './list2tree.util'; +export * from './permission.util'; +export * from './redis.util'; +export * from './schedule.util'; +export * from './tool.util'; diff --git a/src/utils/ip.util.ts b/src/utils/ip.util.ts new file mode 100644 index 0000000..58682ec --- /dev/null +++ b/src/utils/ip.util.ts @@ -0,0 +1,62 @@ +/** + * @module utils/ip + * @description IP utility functions + */ +import type { IncomingMessage } from 'node:http'; + +import axios from 'axios'; +import type { FastifyRequest } from 'fastify'; + +/* 判断IP是不是内网 */ +function isLAN(ip: string) { + ip.toLowerCase(); + if (ip === 'localhost') return true; + let a_ip = 0; + if (ip === '') return false; + const aNum = ip.split('.'); + if (aNum.length !== 4) return false; + a_ip += Number.parseInt(aNum[0]) << 24; + a_ip += Number.parseInt(aNum[1]) << 16; + a_ip += Number.parseInt(aNum[2]) << 8; + a_ip += Number.parseInt(aNum[3]) << 0; + a_ip = (a_ip >> 16) & 0xffff; + return ( + a_ip >> 8 === 0x7f || a_ip >> 8 === 0xa || a_ip === 0xc0a8 || (a_ip >= 0xac10 && a_ip <= 0xac1f) + ); +} + +// 是否来自手机的请求 +export function getIsMobile(request: FastifyRequest | IncomingMessage): boolean { + return request.headers['user-agent'].includes('Dart'); +} + +export function getIp(request: FastifyRequest | IncomingMessage) { + const req = request as any; + + let ip: string = + request.headers['x-forwarded-for'] || + request.headers['X-Forwarded-For'] || + request.headers['X-Real-IP'] || + request.headers['x-real-ip'] || + req?.ip || + req?.raw?.connection?.remoteAddress || + req?.raw?.socket?.remoteAddress || + undefined; + if (ip && ip.split(',').length > 0) ip = ip.split(',')[0]; + + return ip; +} + +export async function getIpAddress(ip: string) { + if (isLAN(ip)) return '内网IP'; + try { + let { data } = await axios.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, { + responseType: 'arraybuffer' + }); + data = new TextDecoder('gbk').decode(data); + data = JSON.parse(data); + return data.addr.trim().split(' ').at(0); + } catch (error) { + return '第三方接口请求失败'; + } +} diff --git a/src/utils/is.util.ts b/src/utils/is.util.ts new file mode 100644 index 0000000..f3802cf --- /dev/null +++ b/src/utils/is.util.ts @@ -0,0 +1,5 @@ +export function isExternal(path: string): boolean { + const reg = + /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/; + return reg.test(path); +} diff --git a/src/utils/list2tree.util.ts b/src/utils/list2tree.util.ts new file mode 100644 index 0000000..10541cb --- /dev/null +++ b/src/utils/list2tree.util.ts @@ -0,0 +1,81 @@ +export type TreeNode = T & { + id: number; + parentId: number; + children?: TreeNode[]; +}; + +export type ListNode = T & { + id: number; + parentId: number; +}; + +export function list2Tree( + items: T, + parentId: number | null = null +): TreeNode[] { + return items + .filter(item => item.parentId === parentId) + .map(item => { + const children = list2Tree(items, item.id); + return { + ...item, + ...(children.length ? { children } : null) + }; + }); +} + +/** + * 过滤树,返回列表数据 + * @param treeData + * @param key 用于过滤的字段 + * @param value 用于过滤的值 + * @returns + */ +export function filterTree2List(treeData, key, value) { + const filterChildrenTree = (resTree, treeItem) => { + if (treeItem[key].includes(value)) { + resTree.push(treeItem); + return resTree; + } + if (Array.isArray(treeItem.children)) { + const children = treeItem.children.reduce(filterChildrenTree, []); + + const data = { ...treeItem, children }; + + if (children.length) resTree.push({ ...data }); + } + return resTree; + }; + return treeData.reduce(filterChildrenTree, []); +} + +/** + * 过滤树,并保留原有的结构 + * @param treeData + * @param predicate + * @returns + */ +export function filterTree( + treeData: TreeNode[], + predicate: (data: T) => boolean +): TreeNode[] { + function filter(treeData: TreeNode[]): TreeNode[] { + if (!treeData?.length) return treeData; + + return treeData.filter(data => { + if (!predicate(data)) return false; + + data.children = filter(data.children); + return true; + }); + } + + return filter(treeData) || []; +} + +export function deleteEmptyChildren(arr: any) { + arr?.forEach(node => { + if (node.children?.length === 0) delete node.children; + else deleteEmptyChildren(node.children); + }); +} diff --git a/src/utils/permission.util.ts b/src/utils/permission.util.ts new file mode 100644 index 0000000..a35d086 --- /dev/null +++ b/src/utils/permission.util.ts @@ -0,0 +1,141 @@ +import { ForbiddenException } from '@nestjs/common'; + +import { envBoolean } from '~/global/env'; +import { MenuEntity } from '~/modules/system/menu/menu.entity'; +import { isExternal } from '~/utils/is.util'; + +function createRoute(menu: MenuEntity, _isRoot) { + const commonMeta = { + title: menu.name, + icon: menu.icon, + isExt: menu.isExt, + extOpenMode: menu.extOpenMode, + type: menu.type, + orderNo: menu.orderNo, + show: menu.show, + activeMenu: menu.activeMenu, + status: menu.status, + keepAlive: menu.keepAlive + }; + + if (isExternal(menu.path)) { + return { + id: menu.id, + path: menu.path, + // component: 'IFrame', + name: menu.name, + meta: { ...commonMeta } + }; + } + + // 目录 + if (menu.type === 0) { + return { + id: menu.id, + path: menu.path, + component: menu.component, + name: menu.name, + meta: { ...commonMeta } + }; + } + + return { + id: menu.id, + path: menu.path, + name: menu.name, + component: menu.component, + meta: { + ...commonMeta + } + }; +} + +function filterAsyncRoutes(menus: MenuEntity[], parentRoute) { + const res = []; + + menus.forEach(menu => { + if (menu.type === 2 || !menu.status) { + // 如果是权限或禁用直接跳过 + return; + } + // 根级别菜单渲染 + let realRoute; + if (!parentRoute && !menu.parentId && menu.type === 1) { + // 根菜单 + realRoute = createRoute(menu, true); + } else if (!parentRoute && !menu.parentId && menu.type === 0) { + // 目录 + const childRoutes = filterAsyncRoutes(menus, menu); + realRoute = createRoute(menu, true); + if (childRoutes && childRoutes.length > 0) { + realRoute.redirect = childRoutes[0].path; + realRoute.children = childRoutes; + } + } else if (parentRoute && parentRoute.id === menu.parentId && menu.type === 1) { + // 子菜单 + realRoute = createRoute(menu, false); + } else if (parentRoute && parentRoute.id === menu.parentId && menu.type === 0) { + // 如果还是目录,继续递归 + const childRoute = filterAsyncRoutes(menus, menu); + realRoute = createRoute(menu, false); + if (childRoute && childRoute.length > 0) { + realRoute.redirect = childRoute[0].path; + realRoute.children = childRoute; + } + } + // add curent route + if (realRoute) res.push(realRoute); + }); + return res; +} + +export function generatorRouters(menus: MenuEntity[]) { + return filterAsyncRoutes(menus, null); +} + +// 获取所有菜单以及权限 +function filterMenuToTable(menus: MenuEntity[], parentMenu) { + const res = []; + menus.forEach(menu => { + // 根级别菜单渲染 + let realMenu; + if (!parentMenu && !menu.parentId && menu.type === 1) { + // 根菜单,查找该跟菜单下子菜单,因为可能会包含权限 + const childMenu = filterMenuToTable(menus, menu); + realMenu = { ...menu }; + realMenu.children = childMenu; + } else if (!parentMenu && !menu.parentId && menu.type === 0) { + // 根目录 + const childMenu = filterMenuToTable(menus, menu); + realMenu = { ...menu }; + realMenu.children = childMenu; + } else if (parentMenu && parentMenu.id === menu.parentId && menu.type === 1) { + // 子菜单下继续找是否有子菜单 + const childMenu = filterMenuToTable(menus, menu); + realMenu = { ...menu }; + realMenu.children = childMenu; + } else if (parentMenu && parentMenu.id === menu.parentId && menu.type === 0) { + // 如果还是目录,继续递归 + const childMenu = filterMenuToTable(menus, menu); + realMenu = { ...menu }; + realMenu.children = childMenu; + } else if (parentMenu && parentMenu.id === menu.parentId && menu.type === 2) { + realMenu = { ...menu }; + } + // add curent route + if (realMenu) { + realMenu.pid = menu.id; + res.push(realMenu); + } + }); + return res; +} + +export function generatorMenu(menu: MenuEntity[]) { + return filterMenuToTable(menu, null); +} + +/** 检测是否为演示环境, 如果为演示环境,则拒绝该操作 */ +export function checkIsDemoMode() { + if (envBoolean('IS_DEMO')) throw new ForbiddenException('演示模式下不允许操作'); +} diff --git a/src/utils/redis.util.ts b/src/utils/redis.util.ts new file mode 100644 index 0000000..f18dad5 --- /dev/null +++ b/src/utils/redis.util.ts @@ -0,0 +1,11 @@ +import type { RedisKeys } from '~/constants/cache.constant'; + +type Prefix = 'm-shop'; +const prefix = 'm-shop'; + +export function getRedisKey( + key: T, + ...concatKeys: string[] +): `${Prefix}:${T}${string | ''}` { + return `${prefix}:${key}${concatKeys && concatKeys.length ? `:${concatKeys.join('_')}` : ''}`; +} diff --git a/src/utils/schedule.util.ts b/src/utils/schedule.util.ts new file mode 100644 index 0000000..66c468a --- /dev/null +++ b/src/utils/schedule.util.ts @@ -0,0 +1,96 @@ +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +export function scheduleMicrotask(callback: () => void) { + sleep(0).then(callback); +} + +type NotifyCallback = () => void; + +type NotifyFunction = (callback: () => void) => void; + +type BatchNotifyFunction = (callback: () => void) => void; + +export function createNotifyManager() { + let queue: NotifyCallback[] = []; + let transactions = 0; + let notifyFn: NotifyFunction = callback => { + callback(); + }; + let batchNotifyFn: BatchNotifyFunction = (callback: () => void) => { + callback(); + }; + + const flush = (): void => { + const originalQueue = queue; + queue = []; + if (originalQueue.length) { + scheduleMicrotask(() => { + batchNotifyFn(() => { + originalQueue.forEach(callback => { + notifyFn(callback); + }); + }); + }); + } + }; + + const batch = (callback: () => T): T => { + let result; + transactions++; + try { + result = callback(); + } finally { + transactions--; + if (!transactions) flush(); + } + return result; + }; + + const schedule = (callback: NotifyCallback): void => { + if (transactions) { + queue.push(callback); + } else { + scheduleMicrotask(() => { + notifyFn(callback); + }); + } + }; + + /** + * All calls to the wrapped function will be batched. + */ + const batchCalls = (callback: T): T => { + return ((...args: any[]) => { + schedule(() => { + callback(...args); + }); + }) as any; + }; + + /** + * Use this method to set a custom notify function. + * This can be used to for example wrap notifications with `React.act` while running tests. + */ + const setNotifyFunction = (fn: NotifyFunction) => { + notifyFn = fn; + }; + + /** + * Use this method to set a custom function to batch notifications together into a single tick. + * By default React Query will use the batch function provided by ReactDOM or React Native. + */ + const setBatchNotifyFunction = (fn: BatchNotifyFunction) => { + batchNotifyFn = fn; + }; + + return { + batch, + batchCalls, + schedule, + setNotifyFunction, + setBatchNotifyFunction + } as const; +} + +// SINGLETON +export const scheduleManager = createNotifyManager(); diff --git a/src/utils/tool.util.ts b/src/utils/tool.util.ts new file mode 100644 index 0000000..5f8afbd --- /dev/null +++ b/src/utils/tool.util.ts @@ -0,0 +1,78 @@ +import { customAlphabet, nanoid } from 'nanoid'; + +import { md5 } from './crypto.util'; +import { add, subtract, multiply, divide, bignumber, BigNumber } from 'mathjs'; + +export function getAvatar(mail: string | undefined) { + if (!mail) return ''; + + return `https://cravatar.cn/avatar/${md5(mail)}?d=retro`; +} + +export function generateUUID(size: number = 21): string { + return nanoid(size); +} + +export function generateShortUUID(): string { + return nanoid(10); +} + +/** + * 生成一个随机的值 + */ +export function generateRandomValue( + length: number, + placeholder = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM' +): string { + const customNanoid = customAlphabet(placeholder, length); + return customNanoid(); +} + +/** + * 生成一个随机的值 + */ +export function randomValue( + size = 16, + dict = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict' +): string { + let id = ''; + let i = size; + const len = dict.length; + while (i--) id += dict[(Math.random() * len) | 0]; + return id; +} + +export const hashString = function (str, seed = 0) { + let h1 = 0xdeadbeef ^ seed; + let h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); + return 4294967296 * (2097151 & h2) + (h1 >>> 0); +}; +/** + * 使用mathjs进行四则运算,不丢失精度 + */ +export function calcNumber( + firstNumber: number, + secondNumber: number, + option: CalclateOption +): number { + switch (option) { + case 'add': + return add(bignumber(firstNumber), bignumber(secondNumber)).toNumber(); + case 'subtract': + return subtract(bignumber(firstNumber), bignumber(secondNumber)).toNumber(); + case 'multiply': + return (multiply(bignumber(firstNumber), bignumber(secondNumber)) as BigNumber).toNumber(); + case 'divide': + return (divide(bignumber(firstNumber), bignumber(secondNumber)) as BigNumber).toNumber(); + default: + return 0; + } +} +type CalclateOption = 'add' | 'subtract' | 'multiply' | 'divide'; diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..e1fc515 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "scripts", "test", "dist", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..48e2daa --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "incremental": true, + "target": "ES2020", + "lib": ["ESNext"], + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "baseUrl": "./", + "module": "commonjs", + "paths": { + "~/*": ["src/*"] + }, + "strictBindCallApply": false, + "strictNullChecks": false, + "noFallthroughCasesInSwitch": false, + "noImplicitAny": false, + "declaration": true, + "outDir": "./dist", + "removeComments": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": false, + "skipLibCheck": true + }, + "exclude": ["node_modules", "scripts", "dist"] +} diff --git a/types/global.d.ts b/types/global.d.ts new file mode 100644 index 0000000..2e5535e --- /dev/null +++ b/types/global.d.ts @@ -0,0 +1,21 @@ +declare global { + interface IAuthUser { + uid: number; + pv: number; + exp?: number; + iat?: number; + roles?: string[]; + } + + export interface IBaseResponse { + message: string; + code: number; + data?: T; + } + + export interface IListRespData { + items: T[]; + } +} + +export {}; diff --git a/types/module.d.ts b/types/module.d.ts new file mode 100644 index 0000000..b6cd022 --- /dev/null +++ b/types/module.d.ts @@ -0,0 +1,7 @@ +import 'fastify'; + +declare module 'fastify' { + interface FastifyRequest { + user?: IAuthUser; + } +} diff --git a/types/utils.d.ts b/types/utils.d.ts new file mode 100644 index 0000000..812c77e --- /dev/null +++ b/types/utils.d.ts @@ -0,0 +1,104 @@ +/** 提取Promise返回值 */ +type UnboxPromise> = T extends Promise + ? U + : never + +/** 将联合类型转为交叉类型 */ +declare type UnionToIntersection = ( + U extends any ? (k: U) => void : never +) extends (k: infer I) => void + ? I + : never + +/** eg: type result = StringToUnion<'abc'> 结果:'a'|'b'|'c' */ +type StringToUnion = S extends `${infer S1}${infer S2}` + ? S1 | StringToUnion + : never + +/** 字符串替换,类似js的字符串replace方法 */ +type Replace< + Str extends string, + From extends string, + To extends string, +> = Str extends `${infer Left}${From}${infer Right}` + ? `${Left}${To}${Right}` + : Str + +/** 字符串替换,类似js的字符串replaceAll方法 */ +type ReplaceAll< + Str extends string, + From extends string, + To extends string, +> = Str extends `${infer Left}${From}${infer Right}` + ? Replace, From, To> + : Str + +/** eg: type result = CamelCase<'foo-bar-baz'>, 结果:fooBarBaz */ +type CamelCase = S extends `${infer S1}-${infer S2}` + ? S2 extends Capitalize + ? `${S1}-${CamelCase}` + : `${S1}${CamelCase>}` + : S + +/** eg: type result = StringToArray<'abc'>, 结果:['a', 'b', 'c'] */ +type StringToArray< + S extends string, + T extends any[] = [], +> = S extends `${infer S1}${infer S2}` ? StringToArray : T + +/** `RequiredKeys`是用来获取所有必填字段,其中这些必填字段组合成一个联合类型 */ +type RequiredKeys = { + [P in keyof T]: T extends Record ? P : never; +}[keyof T] + +/** `OptionalKeys`是用来获取所有可选字段,其中这些可选字段组合成一个联合类型 */ +type OptionalKeys = { + [P in keyof T]: object extends Pick ? P : never; +}[keyof T] + +/** `GetRequired`是用来获取一个类型中,所有必填键及其类型所组成的一个新类型的 */ +type GetRequired = { + [P in RequiredKeys]-?: T[P]; +} + +/** `GetOptional`是用来获取一个类型中,所有可选键及其类型所组成的一个新类型的 */ +type GetOptional = { + [P in OptionalKeys]?: T[P]; +} + +/** type result1 = Includes<[1, 2, 3, 4], '4'> 结果: false; type result2 = Includes<[1, 2, 3, 4], 4> 结果: true */ +type Includes = K extends T[number] ? true : false + +/** eg:type result = MyConcat<[1, 2], [3, 4]> 结果:[1, 2, 3, 4] */ +type MyConcat = [...T, ...U] +/** eg: type result1 = MyPush<[1, 2, 3], 4> 结果:[1, 2, 3, 4] */ +type MyPush = [...T, K] +/** eg: type result3 = MyPop<[1, 2, 3]> 结果:[1, 2] */ +type MyPop = T extends [...infer L, infer R] ? L : never; // eslint-disable-line + +type PropType = string extends Path + ? unknown + : Path extends keyof T + ? T[Path] + : Path extends `${infer K}.${infer R}` + ? K extends keyof T + ? PropType + : unknown + : unknown + +/** + * NestedKeyOf + * Get all the possible paths of an object + * @example + * type Keys = NestedKeyOf<{ a: { b: { c: string } }> + * // 'a' | 'a.b' | 'a.b.c' + */ +type NestedKeyOf = { + [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object + ? `${Key}` | `${Key}.${NestedKeyOf}` + : `${Key}`; +}[keyof ObjectType & (string | number)] + + type RecordNamePaths = { + [K in NestedKeyOf]: PropType + } diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..90de8ed --- /dev/null +++ b/vercel.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "installCommand": "pnpm install", + "buildCommand": "pnpm build", + // 由于项目中使用了路径别名,暂时无法简单的部署到 vercel: https://github.com/vercel/vercel/issues/2832 + "builds": [ + { + "src": "src/main.ts", + "use": "@vercel/node" + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "src/main.ts", + "methods": ["GET", "POST", "PUT", "PATCH", "DELETE"] + } + ], + "env": { + "NODE_ENV": "development" + } +} diff --git a/wait-for-it.sh b/wait-for-it.sh new file mode 100644 index 0000000..3974640 --- /dev/null +++ b/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi \ No newline at end of file