From f3b09bbed43174e1827c11ee6cb163d3f483a484 Mon Sep 17 00:00:00 2001 From: yixr <1> Date: Wed, 16 Oct 2024 11:30:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=AC=E5=9C=B0oa-backup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cz-config.js | 94 + .dockerignore | 46 + .env | 40 + .env.development | 35 + .env.production | 38 + .eslintignore | 8 + .eslintrc.cjs | 22 + .gitattributes | 11 + .gitignore | 78 + .husky/commit-msg | 4 + .husky/pre-commit | 7 + .npmrc | 6 + .prettierignore | 11 + .prettierrc.cjs | 11 + .versionrc.js | 19 + .vscode/launch.json | 17 + CHANGELOG.md | 0 Dockerfile | 55 + LICENSE | 21 + README.md | 110 + commitlint.config.cjs | 24 + deploy.sh | 9 + docker-compose.yml | 68 + ecosystem.config.js | 22 + eslint.config.js | 36 + init_data/sql/hxoa.sql | 1090 ++ minio.js | 14 + nest-cli.json | 17 + package.json | 176 + pnpm-lock.yaml | 12671 ++++++++++++++++ public/upload/.gitkeep | 0 scripts/genEnvTypes.ts | 49 + scripts/resetScheduler.ts | 26 + src/app.module.ts | 98 + src/assets/templates/verification-code-zh.hbs | 4 + src/assets/templates/verification-code.hbs | 5 + src/common/adapters/fastify.adapter.ts | 45 + src/common/adapters/socket.adapter.ts | 26 + src/common/decorators/api-result.decorator.ts | 78 + src/common/decorators/bypass.decorator.ts | 10 + src/common/decorators/cookie.decorator.ts | 8 + src/common/decorators/cron-once.decorator.ts | 19 + src/common/decorators/domain.decorator.ts | 18 + src/common/decorators/field.decorator.ts | 108 + src/common/decorators/http.decorator.ts | 31 + src/common/decorators/id-param.decorator.ts | 13 + .../decorators/idempotence.decorator.ts | 15 + src/common/decorators/swagger.decorator.ts | 11 + src/common/decorators/transform.decorator.ts | 137 + src/common/dto/cursor.dto.ts | 26 + src/common/dto/delete.dto.ts | 8 + src/common/dto/id.dto.ts | 6 + src/common/dto/pager.dto.ts | 45 + src/common/entity/common.entity.ts | 56 + src/common/exceptions/biz.exception.ts | 40 + src/common/exceptions/not-found.exception.ts | 10 + src/common/exceptions/socket.exception.ts | 38 + src/common/filters/any-exception.filter.ts | 80 + .../interceptors/idempotence.interceptor.ts | 134 + .../interceptors/logging.interceptor.ts | 24 + .../interceptors/timeout.interceptor.ts | 25 + .../interceptors/transform.interceptor.ts | 39 + src/common/model/response.model.ts | 39 + src/common/pipes/parse-int.pipe.ts | 12 + src/config/app.config.ts | 20 + src/config/database.config.ts | 37 + src/config/index.ts | 37 + src/config/mailer.config.ts | 18 + src/config/oss.config.ts | 34 + src/config/redis.config.ts | 14 + src/config/security.config.ts | 14 + src/config/swagger.config.ts | 12 + src/constants/cache.constant.ts | 8 + src/constants/enum/index.ts | 48 + src/constants/error-code.constant.ts | 72 + src/constants/event-bus.constant.ts | 4 + src/constants/oss.constant.ts | 8 + src/constants/response.constant.ts | 15 + src/constants/system.constant.ts | 6 + src/global/env.ts | 62 + src/helper/catchError.ts | 5 + src/helper/crud/base.service.ts | 35 + src/helper/crud/crud.factory.ts | 80 + src/helper/genRedisKey.ts | 19 + src/helper/paginate/create-pagination.ts | 26 + src/helper/paginate/index.ts | 141 + src/helper/paginate/interface.ts | 27 + src/helper/paginate/pagination.ts | 11 + src/main.ts | 88 + src/migrations/1707996695540-initData.ts | 14 + src/modules/auth/auth.constant.ts | 26 + src/modules/auth/auth.controller.ts | 60 + src/modules/auth/auth.module.ts | 58 + src/modules/auth/auth.service.ts | 162 + .../auth/controllers/account.controller.ts | 79 + .../auth/controllers/captcha.controller.ts | 48 + .../auth/controllers/email.controller.ts | 38 + .../auth/decorators/allow-anon.decorator.ts | 8 + .../auth/decorators/auth-user.decorator.ts | 15 + .../auth/decorators/permission.decorator.ts | 63 + .../auth/decorators/public.decorator.ts | 8 + .../auth/decorators/resource.decorator.ts | 22 + src/modules/auth/dto/account.dto.ts | 72 + src/modules/auth/dto/auth.dto.ts | 42 + src/modules/auth/dto/captcha.dto.ts | 47 + .../auth/entities/access-token.entity.ts | 40 + .../auth/entities/refresh-token.entity.ts | 32 + src/modules/auth/guards/jwt-auth.guard.ts | 94 + src/modules/auth/guards/local.guard.ts | 11 + src/modules/auth/guards/rbac.guard.ts | 64 + src/modules/auth/guards/resource.guard.ts | 81 + src/modules/auth/models/auth.model.ts | 14 + src/modules/auth/services/captcha.service.ts | 35 + src/modules/auth/services/token.service.ts | 150 + src/modules/auth/strategies/jwt.strategy.ts | 22 + src/modules/auth/strategies/local.strategy.ts | 21 + src/modules/common/base.service.ts | 11 + src/modules/company/company.controller.ts | 80 + src/modules/company/company.dto.ts | 53 + src/modules/company/company.entity.ts | 39 + src/modules/company/company.module.ts | 14 + src/modules/company/company.service.ts | 124 + src/modules/contract/contract.controller.ts | 80 + src/modules/contract/contract.dto.ts | 72 + src/modules/contract/contract.entity.ts | 62 + src/modules/contract/contract.module.ts | 13 + src/modules/contract/contract.service.ts | 145 + src/modules/domian/domain.controller.ts | 67 + src/modules/domian/domain.dto.ts | 12 + src/modules/domian/domain.entity.ts | 15 + src/modules/domian/domain.module.ts | 12 + src/modules/domian/domain.service.ts | 95 + src/modules/health/health.controller.ts | 71 + src/modules/health/health.module.ts | 11 + .../in_out/materials_in_out.controller.ts | 90 + .../in_out/materials_in_out.dto.ts | 199 + .../in_out/materials_in_out.entity.ts | 148 + .../in_out/materials_in_out.service.ts | 465 + .../materials_inventory.controller.ts | 80 + .../materials_inventory.dto.ts | 74 + .../materials_inventory.entity.ts | 98 + .../materials_inventory.module.ts | 24 + .../materials_inventory.service.ts | 571 + .../netdisk/manager/manage-qiniu.service.ts | 836 + src/modules/netdisk/manager/manage.class.ts | 67 + .../netdisk/manager/manage.controller.ts | 135 + src/modules/netdisk/manager/manage.dto.ts | 159 + src/modules/netdisk/manager/manage.service.ts | 794 + src/modules/netdisk/minio/minio.service.ts | 8 + src/modules/netdisk/netdisk.module.ts | 89 + .../netdisk/overview/overview.controller.ts | 41 + src/modules/netdisk/overview/overview.dto.ts | 53 + .../netdisk/overview/overview.service.ts | 165 + src/modules/product/product.controller.ts | 80 + src/modules/product/product.dto.ts | 62 + src/modules/product/product.entity.ts | 109 + src/modules/product/product.module.ts | 14 + src/modules/product/product.service.ts | 194 + src/modules/project/project.controller.ts | 80 + src/modules/project/project.dto.ts | 42 + src/modules/project/project.entity.ts | 43 + src/modules/project/project.module.ts | 14 + src/modules/project/project.service.ts | 122 + .../sale_quotation_component.controller.ts | 69 + .../component/sale_quotation_component.dto.ts | 59 + .../sale_quotation_component.entity.ts | 85 + .../sale_quotation_component.module.ts | 14 + .../sale_quotation_component.service.ts | 161 + .../group/sale_quotation_group.controller.ts | 63 + .../group/sale_quotation_group.dto.ts | 27 + .../group/sale_quotation_group.entity.ts | 35 + .../group/sale_quotation_group.module.ts | 14 + .../group/sale_quotation_group.service.ts | 83 + .../sale_quotation.controller.ts | 24 + .../sale_quotation/sale_quotation.module.ts | 32 + .../sale_quotation/sale_quotation.service.ts | 111 + .../sale_quotation_template.controller.ts | 63 + .../template/sale_quotation_template.dto.ts | 29 + .../sale_quotation_template.entity.ts | 32 + .../sale_quotation_template.module.ts | 14 + .../sale_quotation_template.service.ts | 96 + src/modules/sse/sse.controller.ts | 66 + src/modules/sse/sse.module.ts | 12 + src/modules/sse/sse.service.ts | 85 + src/modules/system/dept/dept.controller.ts | 79 + src/modules/system/dept/dept.dto.ts | 70 + src/modules/system/dept/dept.entity.ts | 28 + src/modules/system/dept/dept.module.ts | 19 + src/modules/system/dept/dept.service.ts | 126 + .../system/dict-item/dict-item.controller.ts | 68 + src/modules/system/dict-item/dict-item.dto.ts | 48 + .../system/dict-item/dict-item.entity.ts | 32 + .../system/dict-item/dict-item.module.ts | 16 + .../system/dict-item/dict-item.service.ts | 102 + .../system/dict-type/dict-type.controller.ts | 75 + src/modules/system/dict-type/dict-type.dto.ts | 51 + .../system/dict-type/dict-type.entity.ts | 29 + .../system/dict-type/dict-type.module.ts | 16 + .../system/dict-type/dict-type.service.ts | 94 + src/modules/system/log/dto/log.dto.ts | 65 + .../system/log/entities/captcha-log.entity.ts | 23 + src/modules/system/log/entities/index.ts | 0 .../system/log/entities/login-log.entity.ts | 29 + .../system/log/entities/task-log.entity.ts | 25 + src/modules/system/log/log.controller.ts | 56 + src/modules/system/log/log.module.ts | 25 + src/modules/system/log/models/log.model.ts | 47 + .../log/services/captcha-log.service.ts | 50 + .../system/log/services/login-log.service.ts | 89 + .../system/log/services/task-log.service.ts | 47 + src/modules/system/menu/menu.controller.ts | 109 + src/modules/system/menu/menu.dto.ts | 101 + src/modules/system/menu/menu.entity.ts | 58 + src/modules/system/menu/menu.model.ts | 8 + src/modules/system/menu/menu.module.ts | 20 + src/modules/system/menu/menu.service.ts | 262 + .../system/online/online.controller.ts | 45 + src/modules/system/online/online.dto.ts | 8 + src/modules/system/online/online.model.ts | 27 + src/modules/system/online/online.module.ts | 27 + src/modules/system/online/online.service.ts | 127 + .../param-config/param-config.controller.ts | 70 + .../system/param-config/param-config.dto.ts | 31 + .../param-config/param-config.entity.ts | 23 + .../param-config/param-config.module.ts | 16 + .../param-config/param-config.service.ts | 89 + src/modules/system/role/role.controller.ts | 84 + src/modules/system/role/role.dto.ts | 43 + src/modules/system/role/role.entity.ts | 43 + src/modules/system/role/role.model.ts | 8 + src/modules/system/role/role.module.ts | 20 + src/modules/system/role/role.service.ts | 160 + src/modules/system/serve/serve.controller.ts | 31 + src/modules/system/serve/serve.model.ts | 86 + src/modules/system/serve/serve.module.ts | 16 + src/modules/system/serve/serve.service.ts | 63 + src/modules/system/system.module.ts | 45 + src/modules/system/task/constant.ts | 12 + src/modules/system/task/task.controller.ts | 98 + src/modules/system/task/task.dto.ts | 103 + src/modules/system/task/task.entity.ts | 55 + src/modules/system/task/task.module.ts | 37 + src/modules/system/task/task.processor.ts | 43 + src/modules/system/task/task.service.ts | 332 + src/modules/system/task/task.ts | 2 + src/modules/tasks/jobs/email.job.ts | 28 + src/modules/tasks/jobs/http-request.job.ts | 31 + src/modules/tasks/jobs/log-clear.job.ts | 26 + src/modules/tasks/mission.decorator.ts | 8 + src/modules/tasks/tasks.module.ts | 46 + src/modules/todo/todo.controller.ts | 69 + src/modules/todo/todo.dto.ts | 14 + src/modules/todo/todo.entity.ts | 20 + src/modules/todo/todo.module.ts | 16 + src/modules/todo/todo.service.ts | 42 + src/modules/tools/email/email.controller.ts | 22 + src/modules/tools/email/email.dto.ts | 19 + src/modules/tools/email/email.module.ts | 9 + .../tools/storage/storage.controller.ts | 41 + src/modules/tools/storage/storage.dto.ts | 91 + src/modules/tools/storage/storage.entity.ts | 86 + src/modules/tools/storage/storage.modal.ts | 27 + src/modules/tools/storage/storage.module.ts | 18 + src/modules/tools/storage/storage.service.ts | 112 + src/modules/tools/tools.module.ts | 24 + src/modules/tools/upload/file.constraint.ts | 51 + src/modules/tools/upload/upload.controller.ts | 51 + src/modules/tools/upload/upload.dto.ts | 22 + src/modules/tools/upload/upload.module.ts | 16 + src/modules/tools/upload/upload.service.ts | 60 + src/modules/user/constant.ts | 4 + src/modules/user/dto/password.dto.ts | 39 + src/modules/user/dto/user.dto.ts | 104 + src/modules/user/user.controller.ts | 98 + src/modules/user/user.entity.ts | 96 + src/modules/user/user.model.ts | 21 + src/modules/user/user.module.ts | 21 + src/modules/user/user.service.ts | 359 + .../vehicle_usage/vehicle_usage.controller.ts | 80 + .../vehicle_usage/vehicle_usage.dto.ts | 81 + .../vehicle_usage/vehicle_usage.entity.ts | 103 + .../vehicle_usage/vehicle_usage.module.ts | 13 + .../vehicle_usage/vehicle_usage.service.ts | 146 + src/repl.ts | 11 + src/setup-swagger.ts | 43 + .../constraints/entity-exist.constraint.ts | 82 + .../database/constraints/unique.constraint.ts | 90 + src/shared/database/database.module.ts | 50 + src/shared/database/field-search/index.ts | 38 + src/shared/database/typeorm-logger.ts | 103 + src/shared/helper/cron.service.ts | 44 + src/shared/helper/helper.module.ts | 14 + src/shared/helper/qq.service.ts | 19 + src/shared/logger/logger.module.ts | 15 + src/shared/logger/logger.service.ts | 111 + src/shared/mailer/mailer.module.ts | 40 + src/shared/mailer/mailer.service.ts | 128 + src/shared/redis/cache.service.ts | 67 + src/shared/redis/redis-subpub.ts | 65 + src/shared/redis/redis.constant.ts | 1 + src/shared/redis/redis.module.ts | 60 + src/shared/redis/subpub.service.ts | 21 + src/shared/shared.module.ts | 49 + src/socket/base.gateway.ts | 25 + src/socket/business-event.constant.ts | 11 + src/socket/events/admin.gateway.ts | 38 + src/socket/events/web.gateway.ts | 37 + src/socket/shared/auth.gateway.ts | 114 + src/socket/socket.module.ts | 16 + src/utils/captcha.util.ts | 19 + src/utils/crypto.util.ts | 28 + src/utils/date.util.ts | 23 + src/utils/file.util.ts | 85 + src/utils/index.ts | 11 + src/utils/ip.util.ts | 62 + src/utils/is.util.ts | 5 + src/utils/list2tree.util.ts | 81 + src/utils/permission.util.ts | 141 + src/utils/redis.util.ts | 11 + src/utils/schedule.util.ts | 96 + src/utils/tool.util.ts | 78 + tsconfig.build.json | 4 + tsconfig.json | 27 + types/global.d.ts | 21 + types/module.d.ts | 7 + types/utils.d.ts | 104 + vercel.json | 22 + wait-for-it.sh | 182 + 328 files changed, 33240 insertions(+) create mode 100644 .cz-config.js create mode 100644 .dockerignore create mode 100644 .env create mode 100644 .env.development create mode 100644 .env.production create mode 100644 .eslintignore create mode 100644 .eslintrc.cjs create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .husky/commit-msg create mode 100644 .husky/pre-commit create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 .prettierrc.cjs create mode 100644 .versionrc.js create mode 100644 .vscode/launch.json create mode 100644 CHANGELOG.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 commitlint.config.cjs create mode 100644 deploy.sh create mode 100644 docker-compose.yml create mode 100644 ecosystem.config.js create mode 100644 eslint.config.js create mode 100644 init_data/sql/hxoa.sql create mode 100644 minio.js create mode 100644 nest-cli.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 public/upload/.gitkeep create mode 100644 scripts/genEnvTypes.ts create mode 100644 scripts/resetScheduler.ts create mode 100644 src/app.module.ts create mode 100644 src/assets/templates/verification-code-zh.hbs create mode 100644 src/assets/templates/verification-code.hbs create mode 100644 src/common/adapters/fastify.adapter.ts create mode 100644 src/common/adapters/socket.adapter.ts create mode 100644 src/common/decorators/api-result.decorator.ts create mode 100644 src/common/decorators/bypass.decorator.ts create mode 100644 src/common/decorators/cookie.decorator.ts create mode 100644 src/common/decorators/cron-once.decorator.ts create mode 100644 src/common/decorators/domain.decorator.ts create mode 100644 src/common/decorators/field.decorator.ts create mode 100644 src/common/decorators/http.decorator.ts create mode 100644 src/common/decorators/id-param.decorator.ts create mode 100644 src/common/decorators/idempotence.decorator.ts create mode 100644 src/common/decorators/swagger.decorator.ts create mode 100644 src/common/decorators/transform.decorator.ts create mode 100644 src/common/dto/cursor.dto.ts create mode 100644 src/common/dto/delete.dto.ts create mode 100644 src/common/dto/id.dto.ts create mode 100644 src/common/dto/pager.dto.ts create mode 100644 src/common/entity/common.entity.ts create mode 100644 src/common/exceptions/biz.exception.ts create mode 100644 src/common/exceptions/not-found.exception.ts create mode 100644 src/common/exceptions/socket.exception.ts create mode 100644 src/common/filters/any-exception.filter.ts create mode 100644 src/common/interceptors/idempotence.interceptor.ts create mode 100644 src/common/interceptors/logging.interceptor.ts create mode 100644 src/common/interceptors/timeout.interceptor.ts create mode 100644 src/common/interceptors/transform.interceptor.ts create mode 100644 src/common/model/response.model.ts create mode 100644 src/common/pipes/parse-int.pipe.ts create mode 100644 src/config/app.config.ts create mode 100644 src/config/database.config.ts create mode 100644 src/config/index.ts create mode 100644 src/config/mailer.config.ts create mode 100644 src/config/oss.config.ts create mode 100644 src/config/redis.config.ts create mode 100644 src/config/security.config.ts create mode 100644 src/config/swagger.config.ts create mode 100644 src/constants/cache.constant.ts create mode 100644 src/constants/enum/index.ts create mode 100644 src/constants/error-code.constant.ts create mode 100644 src/constants/event-bus.constant.ts create mode 100644 src/constants/oss.constant.ts create mode 100644 src/constants/response.constant.ts create mode 100644 src/constants/system.constant.ts create mode 100644 src/global/env.ts create mode 100644 src/helper/catchError.ts create mode 100644 src/helper/crud/base.service.ts create mode 100644 src/helper/crud/crud.factory.ts create mode 100644 src/helper/genRedisKey.ts create mode 100644 src/helper/paginate/create-pagination.ts create mode 100644 src/helper/paginate/index.ts create mode 100644 src/helper/paginate/interface.ts create mode 100644 src/helper/paginate/pagination.ts create mode 100644 src/main.ts create mode 100644 src/migrations/1707996695540-initData.ts create mode 100644 src/modules/auth/auth.constant.ts create mode 100644 src/modules/auth/auth.controller.ts create mode 100644 src/modules/auth/auth.module.ts create mode 100644 src/modules/auth/auth.service.ts create mode 100644 src/modules/auth/controllers/account.controller.ts create mode 100644 src/modules/auth/controllers/captcha.controller.ts create mode 100644 src/modules/auth/controllers/email.controller.ts create mode 100644 src/modules/auth/decorators/allow-anon.decorator.ts create mode 100644 src/modules/auth/decorators/auth-user.decorator.ts create mode 100644 src/modules/auth/decorators/permission.decorator.ts create mode 100644 src/modules/auth/decorators/public.decorator.ts create mode 100644 src/modules/auth/decorators/resource.decorator.ts create mode 100644 src/modules/auth/dto/account.dto.ts create mode 100644 src/modules/auth/dto/auth.dto.ts create mode 100644 src/modules/auth/dto/captcha.dto.ts create mode 100644 src/modules/auth/entities/access-token.entity.ts create mode 100644 src/modules/auth/entities/refresh-token.entity.ts create mode 100644 src/modules/auth/guards/jwt-auth.guard.ts create mode 100644 src/modules/auth/guards/local.guard.ts create mode 100644 src/modules/auth/guards/rbac.guard.ts create mode 100644 src/modules/auth/guards/resource.guard.ts create mode 100644 src/modules/auth/models/auth.model.ts create mode 100644 src/modules/auth/services/captcha.service.ts create mode 100644 src/modules/auth/services/token.service.ts create mode 100644 src/modules/auth/strategies/jwt.strategy.ts create mode 100644 src/modules/auth/strategies/local.strategy.ts create mode 100644 src/modules/common/base.service.ts create mode 100644 src/modules/company/company.controller.ts create mode 100644 src/modules/company/company.dto.ts create mode 100644 src/modules/company/company.entity.ts create mode 100644 src/modules/company/company.module.ts create mode 100644 src/modules/company/company.service.ts create mode 100644 src/modules/contract/contract.controller.ts create mode 100644 src/modules/contract/contract.dto.ts create mode 100644 src/modules/contract/contract.entity.ts create mode 100644 src/modules/contract/contract.module.ts create mode 100644 src/modules/contract/contract.service.ts create mode 100644 src/modules/domian/domain.controller.ts create mode 100644 src/modules/domian/domain.dto.ts create mode 100644 src/modules/domian/domain.entity.ts create mode 100644 src/modules/domian/domain.module.ts create mode 100644 src/modules/domian/domain.service.ts create mode 100644 src/modules/health/health.controller.ts create mode 100644 src/modules/health/health.module.ts create mode 100644 src/modules/materials_inventory/in_out/materials_in_out.controller.ts create mode 100644 src/modules/materials_inventory/in_out/materials_in_out.dto.ts create mode 100644 src/modules/materials_inventory/in_out/materials_in_out.entity.ts create mode 100644 src/modules/materials_inventory/in_out/materials_in_out.service.ts create mode 100644 src/modules/materials_inventory/materials_inventory.controller.ts create mode 100644 src/modules/materials_inventory/materials_inventory.dto.ts create mode 100644 src/modules/materials_inventory/materials_inventory.entity.ts create mode 100644 src/modules/materials_inventory/materials_inventory.module.ts create mode 100644 src/modules/materials_inventory/materials_inventory.service.ts create mode 100644 src/modules/netdisk/manager/manage-qiniu.service.ts create mode 100644 src/modules/netdisk/manager/manage.class.ts create mode 100644 src/modules/netdisk/manager/manage.controller.ts create mode 100644 src/modules/netdisk/manager/manage.dto.ts create mode 100644 src/modules/netdisk/manager/manage.service.ts create mode 100644 src/modules/netdisk/minio/minio.service.ts create mode 100644 src/modules/netdisk/netdisk.module.ts create mode 100644 src/modules/netdisk/overview/overview.controller.ts create mode 100644 src/modules/netdisk/overview/overview.dto.ts create mode 100644 src/modules/netdisk/overview/overview.service.ts create mode 100644 src/modules/product/product.controller.ts create mode 100644 src/modules/product/product.dto.ts create mode 100644 src/modules/product/product.entity.ts create mode 100644 src/modules/product/product.module.ts create mode 100644 src/modules/product/product.service.ts create mode 100644 src/modules/project/project.controller.ts create mode 100644 src/modules/project/project.dto.ts create mode 100644 src/modules/project/project.entity.ts create mode 100644 src/modules/project/project.module.ts create mode 100644 src/modules/project/project.service.ts create mode 100644 src/modules/sale_quotation/component/sale_quotation_component.controller.ts create mode 100644 src/modules/sale_quotation/component/sale_quotation_component.dto.ts create mode 100644 src/modules/sale_quotation/component/sale_quotation_component.entity.ts create mode 100644 src/modules/sale_quotation/component/sale_quotation_component.module.ts create mode 100644 src/modules/sale_quotation/component/sale_quotation_component.service.ts create mode 100644 src/modules/sale_quotation/group/sale_quotation_group.controller.ts create mode 100644 src/modules/sale_quotation/group/sale_quotation_group.dto.ts create mode 100644 src/modules/sale_quotation/group/sale_quotation_group.entity.ts create mode 100644 src/modules/sale_quotation/group/sale_quotation_group.module.ts create mode 100644 src/modules/sale_quotation/group/sale_quotation_group.service.ts create mode 100644 src/modules/sale_quotation/sale_quotation.controller.ts create mode 100644 src/modules/sale_quotation/sale_quotation.module.ts create mode 100644 src/modules/sale_quotation/sale_quotation.service.ts create mode 100644 src/modules/sale_quotation/template/sale_quotation_template.controller.ts create mode 100644 src/modules/sale_quotation/template/sale_quotation_template.dto.ts create mode 100644 src/modules/sale_quotation/template/sale_quotation_template.entity.ts create mode 100644 src/modules/sale_quotation/template/sale_quotation_template.module.ts create mode 100644 src/modules/sale_quotation/template/sale_quotation_template.service.ts create mode 100644 src/modules/sse/sse.controller.ts create mode 100644 src/modules/sse/sse.module.ts create mode 100644 src/modules/sse/sse.service.ts create mode 100644 src/modules/system/dept/dept.controller.ts create mode 100644 src/modules/system/dept/dept.dto.ts create mode 100644 src/modules/system/dept/dept.entity.ts create mode 100644 src/modules/system/dept/dept.module.ts create mode 100644 src/modules/system/dept/dept.service.ts create mode 100644 src/modules/system/dict-item/dict-item.controller.ts create mode 100644 src/modules/system/dict-item/dict-item.dto.ts create mode 100644 src/modules/system/dict-item/dict-item.entity.ts create mode 100644 src/modules/system/dict-item/dict-item.module.ts create mode 100644 src/modules/system/dict-item/dict-item.service.ts create mode 100644 src/modules/system/dict-type/dict-type.controller.ts create mode 100644 src/modules/system/dict-type/dict-type.dto.ts create mode 100644 src/modules/system/dict-type/dict-type.entity.ts create mode 100644 src/modules/system/dict-type/dict-type.module.ts create mode 100644 src/modules/system/dict-type/dict-type.service.ts create mode 100644 src/modules/system/log/dto/log.dto.ts create mode 100644 src/modules/system/log/entities/captcha-log.entity.ts create mode 100644 src/modules/system/log/entities/index.ts create mode 100644 src/modules/system/log/entities/login-log.entity.ts create mode 100644 src/modules/system/log/entities/task-log.entity.ts create mode 100644 src/modules/system/log/log.controller.ts create mode 100644 src/modules/system/log/log.module.ts create mode 100644 src/modules/system/log/models/log.model.ts create mode 100644 src/modules/system/log/services/captcha-log.service.ts create mode 100644 src/modules/system/log/services/login-log.service.ts create mode 100644 src/modules/system/log/services/task-log.service.ts create mode 100644 src/modules/system/menu/menu.controller.ts create mode 100644 src/modules/system/menu/menu.dto.ts create mode 100644 src/modules/system/menu/menu.entity.ts create mode 100644 src/modules/system/menu/menu.model.ts create mode 100644 src/modules/system/menu/menu.module.ts create mode 100644 src/modules/system/menu/menu.service.ts create mode 100644 src/modules/system/online/online.controller.ts create mode 100644 src/modules/system/online/online.dto.ts create mode 100644 src/modules/system/online/online.model.ts create mode 100644 src/modules/system/online/online.module.ts create mode 100644 src/modules/system/online/online.service.ts create mode 100644 src/modules/system/param-config/param-config.controller.ts create mode 100644 src/modules/system/param-config/param-config.dto.ts create mode 100644 src/modules/system/param-config/param-config.entity.ts create mode 100644 src/modules/system/param-config/param-config.module.ts create mode 100644 src/modules/system/param-config/param-config.service.ts create mode 100644 src/modules/system/role/role.controller.ts create mode 100644 src/modules/system/role/role.dto.ts create mode 100644 src/modules/system/role/role.entity.ts create mode 100644 src/modules/system/role/role.model.ts create mode 100644 src/modules/system/role/role.module.ts create mode 100644 src/modules/system/role/role.service.ts create mode 100644 src/modules/system/serve/serve.controller.ts create mode 100644 src/modules/system/serve/serve.model.ts create mode 100644 src/modules/system/serve/serve.module.ts create mode 100644 src/modules/system/serve/serve.service.ts create mode 100644 src/modules/system/system.module.ts create mode 100644 src/modules/system/task/constant.ts create mode 100644 src/modules/system/task/task.controller.ts create mode 100644 src/modules/system/task/task.dto.ts create mode 100644 src/modules/system/task/task.entity.ts create mode 100644 src/modules/system/task/task.module.ts create mode 100644 src/modules/system/task/task.processor.ts create mode 100644 src/modules/system/task/task.service.ts create mode 100644 src/modules/system/task/task.ts create mode 100644 src/modules/tasks/jobs/email.job.ts create mode 100644 src/modules/tasks/jobs/http-request.job.ts create mode 100644 src/modules/tasks/jobs/log-clear.job.ts create mode 100644 src/modules/tasks/mission.decorator.ts create mode 100644 src/modules/tasks/tasks.module.ts create mode 100644 src/modules/todo/todo.controller.ts create mode 100644 src/modules/todo/todo.dto.ts create mode 100644 src/modules/todo/todo.entity.ts create mode 100644 src/modules/todo/todo.module.ts create mode 100644 src/modules/todo/todo.service.ts create mode 100644 src/modules/tools/email/email.controller.ts create mode 100644 src/modules/tools/email/email.dto.ts create mode 100644 src/modules/tools/email/email.module.ts create mode 100644 src/modules/tools/storage/storage.controller.ts create mode 100644 src/modules/tools/storage/storage.dto.ts create mode 100644 src/modules/tools/storage/storage.entity.ts create mode 100644 src/modules/tools/storage/storage.modal.ts create mode 100644 src/modules/tools/storage/storage.module.ts create mode 100644 src/modules/tools/storage/storage.service.ts create mode 100644 src/modules/tools/tools.module.ts create mode 100644 src/modules/tools/upload/file.constraint.ts create mode 100644 src/modules/tools/upload/upload.controller.ts create mode 100644 src/modules/tools/upload/upload.dto.ts create mode 100644 src/modules/tools/upload/upload.module.ts create mode 100644 src/modules/tools/upload/upload.service.ts create mode 100644 src/modules/user/constant.ts create mode 100644 src/modules/user/dto/password.dto.ts create mode 100644 src/modules/user/dto/user.dto.ts create mode 100644 src/modules/user/user.controller.ts create mode 100644 src/modules/user/user.entity.ts create mode 100644 src/modules/user/user.model.ts create mode 100644 src/modules/user/user.module.ts create mode 100644 src/modules/user/user.service.ts create mode 100644 src/modules/vehicle_usage/vehicle_usage.controller.ts create mode 100644 src/modules/vehicle_usage/vehicle_usage.dto.ts create mode 100644 src/modules/vehicle_usage/vehicle_usage.entity.ts create mode 100644 src/modules/vehicle_usage/vehicle_usage.module.ts create mode 100644 src/modules/vehicle_usage/vehicle_usage.service.ts create mode 100644 src/repl.ts create mode 100644 src/setup-swagger.ts create mode 100644 src/shared/database/constraints/entity-exist.constraint.ts create mode 100644 src/shared/database/constraints/unique.constraint.ts create mode 100644 src/shared/database/database.module.ts create mode 100644 src/shared/database/field-search/index.ts create mode 100644 src/shared/database/typeorm-logger.ts create mode 100644 src/shared/helper/cron.service.ts create mode 100644 src/shared/helper/helper.module.ts create mode 100644 src/shared/helper/qq.service.ts create mode 100644 src/shared/logger/logger.module.ts create mode 100644 src/shared/logger/logger.service.ts create mode 100644 src/shared/mailer/mailer.module.ts create mode 100644 src/shared/mailer/mailer.service.ts create mode 100644 src/shared/redis/cache.service.ts create mode 100644 src/shared/redis/redis-subpub.ts create mode 100644 src/shared/redis/redis.constant.ts create mode 100644 src/shared/redis/redis.module.ts create mode 100644 src/shared/redis/subpub.service.ts create mode 100644 src/shared/shared.module.ts create mode 100644 src/socket/base.gateway.ts create mode 100644 src/socket/business-event.constant.ts create mode 100644 src/socket/events/admin.gateway.ts create mode 100644 src/socket/events/web.gateway.ts create mode 100644 src/socket/shared/auth.gateway.ts create mode 100644 src/socket/socket.module.ts create mode 100644 src/utils/captcha.util.ts create mode 100644 src/utils/crypto.util.ts create mode 100644 src/utils/date.util.ts create mode 100644 src/utils/file.util.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/ip.util.ts create mode 100644 src/utils/is.util.ts create mode 100644 src/utils/list2tree.util.ts create mode 100644 src/utils/permission.util.ts create mode 100644 src/utils/redis.util.ts create mode 100644 src/utils/schedule.util.ts create mode 100644 src/utils/tool.util.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json create mode 100644 types/global.d.ts create mode 100644 types/module.d.ts create mode 100644 types/utils.d.ts create mode 100644 vercel.json create mode 100644 wait-for-it.sh 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