From 72d7659219cda0da8c3f1d79fdab66dc3ed7aa6d Mon Sep 17 00:00:00 2001 From: lovebird Date: Sat, 25 May 2024 10:36:47 +0200 Subject: [PATCH] init from boilerplate --- .dockerignore | 31 + .env.example | 52 + .eslintignore | 9 + .eslintrc | 23 + .github/dependabot.yml | 14 + .github/workflows/linter.yml | 31 + .github/workflows/release.yml | 45 + .gitignore | 51 +- .husky/pre-commit | 6 + .npmignore | 4 - .prettierrc | 5 + LICENSE | 9 - LICENSE.md | 21 + README.md | 515 +- cspell.json | 60 + docker-compose.yml | 37 + dockerfile | 14 + nest-cli.json | 16 + package.json | 168 +- scripts/docker/dockerfile.prod | 31 + scripts/jenkinsfile | 193 + src/.gitignore | 0 src/app/app.module.ts | 20 + src/app/constants/app.constant.ts | 1 + src/app/constants/app.enum.constant.ts | 5 + src/app/controllers/app.controller.ts | 81 + src/app/docs/app.doc.ts | 30 + .../serializations/app.hello.serialization.ts | 30 + src/cli.ts | 22 + src/common/api-key/api-key.module.ts | 12 + .../api-key/constants/api-key.constant.ts | 1 + src/common/api-key/constants/api-key.doc.ts | 22 + .../constants/api-key.list.constant.ts | 9 + .../constants/api-key.status-code.constant.ts | 10 + .../controllers/api-key.admin.controller.ts | 118 + .../api-key/controllers/api-key.controller.ts | 222 + .../decorators/api-key.admin.decorator.ts | 60 + .../api-key/decorators/api-key.decorator.ts | 27 + src/common/api-key/docs/api-key.admin.doc.ts | 119 + src/common/api-key/dtos/api-key.active.dto.ts | 15 + src/common/api-key/dtos/api-key.create.dto.ts | 57 + .../api-key/dtos/api-key.request.dto.ts | 16 + .../api-key/dtos/api-key.update-date.dto.ts | 32 + src/common/api-key/dtos/api-key.update.dto.ts | 7 + .../api-key/guards/api-key.active.guard.ts | 36 + .../api-key/guards/api-key.expired.guard.ts | 31 + .../api-key/guards/api-key.not-found.guard.ts | 23 + .../guards/api-key.put-to-request.guard.ts | 19 + .../x-api-key/api-key.x-api-key.guard.ts | 86 + .../x-api-key/api-key.x-api-key.strategy.ts | 119 + .../api-key/interfaces/api-key.interface.ts | 12 + .../interfaces/api-key.service.interface.ts | 92 + .../repository/api-key.repository.module.ts | 26 + .../repository/entities/api-key.entity.ts | 85 + .../repositories/api-key.repository.ts | 21 + .../api-key.create.serialization.ts | 14 + .../api-key.get.serialization.ts | 70 + .../api-key.list.serialization.ts | 6 + .../api-key.reset.serialization.ts | 3 + .../api-key/services/api-key.service.ts | 260 + .../api-key/tasks/api-key.inactive.task.ts | 21 + src/common/auth/auth.module.ts | 12 + src/common/auth/constants/auth.constant.ts | 2 + .../auth/constants/auth.enum.constant.ts | 17 + .../auth.enum.permission.constant.ts | 32 + .../constants/auth.status-code.constant.ts | 10 + .../auth/decorators/auth.jwt.decorator.ts | 49 + .../decorators/auth.permission.decorator.ts | 27 + .../jwt-access/auth.jwt-access.guard.ts | 19 + .../jwt-access/auth.jwt-access.strategy.ts | 40 + .../jwt-refresh/auth.jwt-refresh.guard.ts | 19 + .../jwt-refresh/auth.jwt-refresh.strategy.ts | 43 + .../payload/auth.payload.access-for.guard.ts | 48 + .../payload/auth.payload.permission.guard.ts | 57 + .../permission/auth.permission.guard.ts | 81 + src/common/auth/interfaces/auth.interface.ts | 17 + .../auth/interfaces/auth.service.interface.ts | 93 + src/common/auth/services/auth.service.ts | 371 + src/common/aws/aws.module.ts | 10 + src/common/aws/constants/aws.s3.constant.ts | 1 + src/common/aws/interfaces/aws.interface.ts | 6 + .../interfaces/aws.s3-service.interface.ts | 63 + .../aws.s3-multipart.serialization.ts | 53 + .../serializations/aws.s3.serialization.ts | 41 + src/common/aws/services/aws.s3.service.ts | 377 + src/common/common.module.ts | 179 + .../constants/dashboard.doc.constant.ts | 21 + src/common/dashboard/dashboard.module.ts | 10 + src/common/dashboard/dtos/dashboard.ts | 29 + .../interfaces/dashboard.interface.ts | 18 + .../interfaces/dashboard.service.interface.ts | 24 + .../dashboard.month-and-year.serialization.ts | 38 + .../serializations/dashboard.serialization.ts | 12 + .../dashboard/services/dashboard.service.ts | 81 + .../database.base-entity.abstract.ts | 12 + .../database.base-repository.abstract.ts | 116 + ...atabase.mongo.object-id.entity.abstract.ts | 38 + .../database.mongo.uuid.entity.abstract.ts | 37 + ...ase.mongo.object-id.repository.abstract.ts | 746 ++ ...database.mongo.uuid.repository.abstract.ts | 734 ++ .../database/constants/database.constant.ts | 5 + .../constants/database.function.constant.ts | 6 + .../database/database.options.module.ts | 10 + .../database/decorators/database.decorator.ts | 35 + .../database/interfaces/database.interface.ts | 54 + .../database.options-service.interface.ts | 5 + .../services/database.options.service.ts | 60 + .../debugger/constants/debugger.constant.ts | 5 + src/common/debugger/debugger.module.ts | 51 + .../debugger/debugger.options.module.ts | 9 + .../debugger/interfaces/debugger.interface.ts | 22 + .../debugger.options-service.interface.ts | 5 + .../interfaces/debugger.service.interface.ts | 11 + .../middleware/debugger.middleware.module.ts | 21 + .../http/debugger.http.middleware.ts | 170 + .../services/debugger.options.service.ts | 84 + .../debugger/services/debugger.service.ts | 53 + src/common/doc/constants/doc.enum.constant.ts | 11 + src/common/doc/decorators/doc.decorator.ts | 690 ++ src/common/doc/interfaces/doc.interface.ts | 66 + src/common/error/constants/error.constant.ts | 2 + .../error/constants/error.enum.constant.ts | 4 + .../constants/error.status-code.constant.ts | 5 + .../error/decorators/error.decorator.ts | 12 + src/common/error/error.module.ts | 20 + src/common/error/filters/error.http.filter.ts | 186 + src/common/error/guards/error.meta.guard.ts | 31 + .../error/interfaces/error.interface.ts | 48 + .../serializations/error.serialization.ts | 3 + src/common/file/constants/file.constant.ts | 2 + .../file/constants/file.enum.constant.ts | 29 + .../constants/file.status-code.constant.ts | 8 + src/common/file/decorators/file.decorator.ts | 45 + src/common/file/dtos/file.multiple.dto.ts | 6 + src/common/file/dtos/file.single.dto.ts | 6 + .../file.custom-max-files.interceptor.ts | 35 + .../file.custom-max-size.interceptor.ts | 35 + src/common/file/interfaces/file.interface.ts | 6 + src/common/file/pipes/file.extract.pipe.ts | 62 + src/common/file/pipes/file.max-files.pipe.ts | 145 + src/common/file/pipes/file.required.pipe.ts | 27 + src/common/file/pipes/file.size.pipe.ts | 200 + src/common/file/pipes/file.type.pipe.ts | 149 + src/common/file/pipes/file.validation.pipe.ts | 128 + .../helper/constants/helper.enum.constant.ts | 33 + .../constants/helper.function.constant.ts | 5 + src/common/helper/helper.module.ts | 53 + .../helper.array-service.interface.ts | 57 + .../helper.date-service.interface.ts | 99 + .../helper.encryption-service.interface.ts | 33 + .../helper.file-service.interface.ts | 26 + .../helper.geo-service.interface.ts | 8 + .../helper.hash-service.interface.ts | 11 + .../helper/interfaces/helper.interface.ts | 101 + .../helper.number-service.interface.ts | 11 + .../helper.string-service.interface.ts | 19 + .../helper/services/helper.array.service.ts | 119 + .../helper/services/helper.date.service.ts | 244 + .../services/helper.encryption.service.ts | 89 + .../helper/services/helper.file.service.ts | 76 + .../helper/services/helper.geo.service.ts | 18 + .../helper/services/helper.hash.service.ts | 27 + .../helper/services/helper.number.service.ts | 33 + .../helper/services/helper.string.service.ts | 78 + .../logger/constants/logger.constant.ts | 2 + .../logger/constants/logger.enum.constant.ts | 11 + .../logger/decorators/logger.decorator.ts | 19 + src/common/logger/dtos/logger.create.dto.ts | 26 + .../logger/interceptors/logger.interceptor.ts | 91 + .../logger/interfaces/logger.interface.ts | 7 + .../interfaces/logger.service.interface.ts | 17 + src/common/logger/logger.module.ts | 11 + .../repository/entities/logger.entity.ts | 117 + .../repository/logger.repository.module.ts | 26 + .../repositories/logger.repository.ts | 26 + src/common/logger/services/logger.service.ts | 193 + .../constants/message.enum.constant.ts | 4 + .../message/controllers/message.controller.ts | 30 + src/common/message/docs/message.enum.doc.ts | 11 + .../message/interfaces/message.interface.ts | 9 + .../interfaces/message.service.interface.ts | 33 + src/common/message/message.module.ts | 36 + .../message.custom-language.middleware.ts | 51 + .../middleware/message.middleware.module.ts | 9 + .../message.language.serialization.ts | 11 + .../message/services/message.service.ts | 140 + .../constants/pagination.constant.ts | 12 + .../constants/pagination.enum.constant.ts | 14 + .../decorators/pagination.decorator.ts | 85 + .../pagination/dtos/pagination.list.dto.ts | 23 + .../interfaces/pagination.interface.ts | 35 + .../pagination.service.interface.ts | 43 + src/common/pagination/pagination.module.ts | 10 + .../pipes/pagination.filter-contain.pipe.ts | 62 + .../pipes/pagination.filter-date.pipe.ts | 53 + .../pagination.filter-equal-object-id.pipe.ts | 42 + .../pipes/pagination.filter-equal.pipe.ts | 67 + .../pagination.filter-in-boolean.pipe.ts | 45 + .../pipes/pagination.filter-in-enum.pipe.ts | 45 + .../pagination/pipes/pagination.order.pipe.ts | 54 + .../pipes/pagination.paging.pipe.ts | 49 + .../pipes/pagination.search.pipe.ts | 41 + .../pagination/services/pagination.service.ts | 136 + .../request/constants/request.constant.ts | 5 + .../constants/request.enum.constant.ts | 9 + .../constants/request.status-code.constant.ts | 7 + .../request/decorators/request.decorator.ts | 78 + .../request/guards/request.param.guard.ts | 40 + .../request.timeout.interceptor.ts | 81 + .../request.timestamp.interceptor.ts | 76 + .../request.user-agent.interceptor.ts | 68 + .../request/interfaces/request.interface.ts | 27 + .../request.body-parser.middleware.ts | 73 + .../cors/request.cors.middleware.ts | 44 + .../helmet/request.helmet.middleware.ts | 10 + .../middleware/id/request.id.middleware.ts | 18 + .../middleware/request.middleware.module.ts | 36 + .../timestamp/request.timestamp.middleware.ts | 25 + .../timezone/request.timezone.middleware.ts | 15 + .../request.user-agent.middleware.ts | 19 + .../version/request.version.middleware.ts | 53 + src/common/request/request.module.ts | 88 + .../request.pagination.serialization.ts | 16 + .../request.is-password-medium.validation.ts | 40 + .../request.is-password-strong.validation.ts | 40 + .../request.is-password-weak.validation.ts | 38 + .../request.is-start-with.validation.ts | 34 + .../request.max-binary-file.validation.ts | 64 + .../request.max-date-today.validation.ts | 33 + ...quest.max-greater-than-equal.validation.ts | 36 + .../request.max-greater-than.validation.ts | 34 + .../request.min-date-today.validation.ts | 33 + ...quest.min-greater-than-equal.validation.ts | 36 + .../request.min-greater-than.validation.ts | 34 + ...equest.mobile-number-allowed.validation.ts | 38 + .../request.only-digits.validation.ts | 31 + .../request.safe-string.validation.ts | 30 + .../validations/request.skip.validation.ts | 25 + .../response/constants/response.constant.ts | 7 + .../response/decorators/response.decorator.ts | 79 + .../response.custom-headers.interceptor.ts | 41 + .../response.default.interceptor.ts | 147 + .../response.excel.interceptor.ts | 105 + .../response.paging.interceptor.ts | 213 + .../response/interfaces/response.interface.ts | 50 + .../middleware/response.middleware.module.ts | 9 + .../time/response.time.middleware.ts | 10 + src/common/response/response.module.ts | 16 + .../response.default.serialization.ts | 64 + .../response.id.serialization.ts | 12 + .../response.paging.serialization.ts | 72 + .../setting/constants/setting.doc.constant.ts | 19 + .../constants/setting.enum.constant.ts | 6 + .../constants/setting.list.constant.ts | 8 + .../constants/setting.status-code.constant.ts | 4 + .../controllers/setting.admin.controller.ts | 77 + .../setting/controllers/setting.controller.ts | 112 + .../decorators/setting.admin.decorator.ts | 9 + .../setting/decorators/setting.decorator.ts | 31 + src/common/setting/docs/setting.admin.doc.ts | 19 + src/common/setting/docs/setting.doc.ts | 49 + src/common/setting/dtos/setting.create.dto.ts | 48 + .../setting/dtos/setting.request.dto.ts | 16 + .../setting/dtos/setting.update-value.dto.ts | 6 + .../setting/guards/setting.not-found.guard.ts | 24 + .../guards/setting.put-to-request.guard.ts | 39 + .../interfaces/setting.service.interface.ts | 66 + .../setting.maintenance.middleware.ts | 32 + .../middleware/setting.middleware.module.ts | 50 + .../repository/entities/setting.entity.ts | 43 + .../repositories/setting.repository.ts | 21 + .../repository/setting.repository.module.ts | 26 + .../setting.get.serialization.ts | 74 + .../setting.list.serialization.ts | 3 + .../setting/services/setting.service.ts | 170 + src/common/setting/setting.module.ts | 13 + src/configs/app.config.ts | 31 + src/configs/auth.config.ts | 62 + src/configs/aws.config.ts | 16 + src/configs/database.config.ts | 13 + src/configs/debugger.config.ts | 22 + src/configs/doc.config.ts | 11 + src/configs/file.config.ts | 26 + src/configs/helper.config.ts | 16 + src/configs/index.ts | 23 + src/configs/request.config.ts | 95 + src/configs/user.config.ts | 9 + src/health/controllers/health.controller.ts | 117 + src/health/docs/health.doc.ts | 14 + src/health/health.module.ts | 10 + .../indicators/health.aws-s3.indicator.ts | 26 + .../serializations/health.serialization.ts | 31 + src/jobs/jobs.module.ts | 27 + src/jobs/router/jobs.router.module.ts | 11 + src/languages/en/apiKey.json | 13 + src/languages/en/app.json | 4 + src/languages/en/auth.json | 15 + src/languages/en/file.json | 10 + src/languages/en/health.json | 3 + src/languages/en/http.json | 67 + src/languages/en/message.json | 5 + src/languages/en/middleware.json | 7 + src/languages/en/permission.json | 11 + src/languages/en/request.json | 30 + src/languages/en/role.json | 16 + src/languages/en/setting.json | 9 + src/languages/en/user.json | 25 + src/languages/id/app.json | 4 + src/languages/id/request.json | 30 + src/main.ts | 74 + src/migration/migration.module.ts | 34 + src/migration/seeds/migration.api-key.seed.ts | 49 + .../seeds/migration.permission.seed.ts | 55 + src/migration/seeds/migration.role.seed.ts | 61 + src/migration/seeds/migration.setting.seed.ts | 42 + src/migration/seeds/migration.user.seed.ts | 97 + .../constants/permission.constant.ts | 1 + .../constants/permission.doc.constant.ts | 34 + .../constants/permission.enum.constant.ts | 7 + .../constants/permission.list.constant.ts | 15 + .../permission.status-code.constant.ts | 6 + .../permission.admin.controller.ts | 253 + .../decorators/permission.admin.decorator.ts | 44 + .../decorators/permission.decorator.ts | 15 + .../permission/docs/permission.admin.doc.ts | 106 + .../permission/dtos/permission.active.dto.ts | 14 + .../permission/dtos/permission.create.dto.ts | 33 + .../dtos/permission.update-description.dto.ts | 7 + .../dtos/permission.update-group.dto.ts | 6 + .../dtos/permissions.request.dto.ts | 16 + .../guards/permission.active.guard.ts | 36 + .../guards/permission.not-found.guard.ts | 23 + .../guards/permission.put-to-request.guard.ts | 21 + .../interfaces/permission.interface.ts | 7 + .../permission.service.interface.ts | 85 + src/modules/permission/permission.module.ts | 11 + .../repository/entities/permission.entity.ts | 58 + .../permission.repository.module.ts | 26 + .../repositories/permission.repository.ts | 21 + .../permission.get.serialization.ts | 51 + .../permission.group.serialization.ts | 35 + .../permission.list.serialization.ts | 3 + .../permission/services/permission.service.ts | 171 + src/modules/role/constants/role.constant.ts | 1 + .../role/constants/role.doc.constant.ts | 34 + .../role/constants/role.list.constant.ts | 11 + .../constants/role.status-code.constant.ts | 7 + .../role/controllers/role.admin.controller.ts | 381 + .../role/decorators/role.admin.decorator.ts | 48 + src/modules/role/decorators/role.decorator.ts | 12 + src/modules/role/docs/role.admin.doc.ts | 127 + src/modules/role/dtos/role.active.dto.ts | 14 + src/modules/role/dtos/role.create.dto.ts | 46 + src/modules/role/dtos/role.request.dto.ts | 16 + src/modules/role/dtos/role.update-name.dto.ts | 6 + .../role/dtos/role.update-permission.dto.ts | 7 + src/modules/role/guards/role.active.guard.ts | 35 + .../role/guards/role.not-found.guard.ts | 23 + .../role/guards/role.put-to-request.guard.ts | 21 + src/modules/role/guards/role.used.guard.ts | 32 + src/modules/role/interfaces/role.interface.ts | 16 + .../role/interfaces/role.service.interface.ts | 87 + .../role/repository/entities/role.entity.ts | 57 + .../repositories/role.repository.ts | 27 + .../role/repository/role.repository.module.ts | 26 + src/modules/role/role.module.ts | 11 + .../role.access-for.serialization.ts | 11 + .../serializations/role.get.serialization.ts | 55 + .../serializations/role.list.serialization.ts | 16 + src/modules/role/services/role.service.ts | 190 + src/modules/user/constants/user.constant.ts | 2 + .../user/constants/user.doc.constant.ts | 33 + .../user/constants/user.list.constant.ts | 24 + .../constants/user.status-code.constant.ts | 13 + .../user/controllers/user.admin.controller.ts | 414 + .../user/controllers/user.controller.ts | 546 ++ .../controllers/user.public.controller.ts | 119 + .../user/decorators/user.admin.decorator.ts | 42 + src/modules/user/decorators/user.decorator.ts | 12 + .../user/decorators/user.public.decorator.ts | 9 + src/modules/user/docs/user.admin.doc.ts | 158 + src/modules/user/docs/user.doc.ts | 98 + src/modules/user/docs/user.public.doc.ts | 25 + src/modules/user/dtos/user.active.dto.ts | 25 + src/modules/user/dtos/user.block.dto.ts | 24 + .../user/dtos/user.change-password.dto.ts | 29 + src/modules/user/dtos/user.create.dto.ts | 91 + .../user/dtos/user.grant-permission.dto.ts | 17 + src/modules/user/dtos/user.import.dto.ts | 7 + src/modules/user/dtos/user.login.dto.ts | 19 + .../user/dtos/user.password-attempt.dto.ts | 14 + .../user/dtos/user.password-expired.dto.ts | 6 + src/modules/user/dtos/user.password.dto.ts | 46 + src/modules/user/dtos/user.photo.dto.ts | 9 + src/modules/user/dtos/user.request.dto.ts | 16 + src/modules/user/dtos/user.sign-up.dto.ts | 4 + src/modules/user/dtos/user.update-name.dto.ts | 7 + .../user.payload.put-to-request.guard.ts | 20 + src/modules/user/guards/user.active.guard.ts | 35 + src/modules/user/guards/user.blocked.guard.ts | 35 + .../user/guards/user.not-found.guard.ts | 23 + .../user/guards/user.put-to-request.guard.ts | 21 + src/modules/user/interfaces/user.interface.ts | 16 + .../user/interfaces/user.service.interface.ts | 125 + .../user/repository/entities/user.entity.ts | 168 + .../repositories/user.repository.ts | 27 + .../user/repository/user.repository.module.ts | 26 + .../serializations/user.get.serialization.ts | 119 + .../user.grant-permission.serialization.ts | 24 + .../user.import.serialization.ts | 17 + .../serializations/user.info.serialization.ts | 13 + .../serializations/user.list.serialization.ts | 39 + .../user.login.serialization.ts | 32 + .../user.payload-permission.serialization.ts | 20 + .../user.payload.serialization.ts | 81 + .../user.profile.serialization.ts | 10 + src/modules/user/services/user.service.ts | 284 + src/modules/user/user.module.ts | 10 + src/router/router.module.ts | 59 + src/router/routes/routes.admin.module.ts | 31 + src/router/routes/routes.callback.module.ts | 9 + src/router/routes/routes.module.ts | 37 + src/router/routes/routes.public.module.ts | 25 + src/router/routes/routes.test.module.ts | 9 + src/swagger.ts | 82 + test/e2e/api-key/api-key.admin.e2e-spec.ts | 282 + test/e2e/api-key/api-key.constant.ts | 12 + test/e2e/api-key/api-key.e2e-spec.ts | 276 + test/e2e/jest.json | 30 + .../permission/permission.admin.e2e-spec.ts | 285 + test/e2e/permission/permission.constant.ts | 8 + test/e2e/role/role.admin.e2e-spec.ts | 498 + test/e2e/role/role.constant.ts | 10 + test/e2e/setting/setting.admin.e2e-spec.ts | 181 + test/e2e/setting/setting.constant.ts | 6 + test/e2e/setting/setting.e2e-spec.ts | 126 + test/e2e/user/files/import.csv | 2 + test/e2e/user/files/medium.jpg | Bin 0 -> 308701 bytes test/e2e/user/files/small.jpg | Bin 0 -> 52721 bytes test/e2e/user/files/test.txt | 1 + test/e2e/user/user.admin.e2e-spec.ts | 476 + .../e2e/user/user.change-password.e2e-spec.ts | 196 + test/e2e/user/user.constant.ts | 39 + test/e2e/user/user.e2e-spec.ts | 164 + .../user/user.grant-permission.e2e-spec.ts | 245 + test/e2e/user/user.info.e2e-spec.ts | 63 + test/e2e/user/user.login.e2e-spec.ts | 344 + test/e2e/user/user.public.e2e-spec.ts | 171 + test/e2e/user/user.refresh.e2e-spec.ts | 297 + test/integration/aws/aws.s3.constant.ts | 1 + .../aws/aws.s3.integration.spec.ts | 36 + .../integration/database/database.constant.ts | 1 + .../database/database.integration.spec.ts | 36 + test/integration/jest.json | 32 + test/unit/api-key/api-key.service.spec.ts | 503 + test/unit/auth/auth.service.spec.ts | 692 ++ .../database/database.options.service.spec.ts | 122 + .../debugger/debugger.options.service.spec.ts | 199 + test/unit/debugger/debugger.service.spec.ts | 174 + test/unit/helper/helper.array.service.spec.ts | 503 + test/unit/helper/helper.date.service.spec.ts | 974 ++ .../helper/helper.encryption.service.spec.ts | 250 + test/unit/helper/helper.file.service.spec.ts | 112 + test/unit/helper/helper.geo.service.spec.ts | 54 + test/unit/helper/helper.hash.service.spec.ts | 129 + .../unit/helper/helper.number.service.spec.ts | 113 + .../unit/helper/helper.string.service.spec.ts | 229 + test/unit/jest.json | 49 + test/unit/logger/logger.service.spec.ts | 249 + test/unit/message/message.service.spec.ts | 481 + .../pagination/pagination.service.spec.ts | 423 + test/unit/setting/setting.service.spec.ts | 585 ++ tsconfig.build.json | 14 + tsconfig.json | 32 + yarn.lock | 8140 +++++++++++++++++ 475 files changed, 39924 insertions(+), 55 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/linter.yml create mode 100644 .github/workflows/release.yml create mode 100644 .husky/pre-commit delete mode 100644 .npmignore create mode 100644 .prettierrc delete mode 100644 LICENSE create mode 100644 LICENSE.md create mode 100644 cspell.json create mode 100644 docker-compose.yml create mode 100644 dockerfile create mode 100644 nest-cli.json create mode 100644 scripts/docker/dockerfile.prod create mode 100644 scripts/jenkinsfile delete mode 100644 src/.gitignore create mode 100644 src/app/app.module.ts create mode 100644 src/app/constants/app.constant.ts create mode 100644 src/app/constants/app.enum.constant.ts create mode 100644 src/app/controllers/app.controller.ts create mode 100644 src/app/docs/app.doc.ts create mode 100644 src/app/serializations/app.hello.serialization.ts create mode 100644 src/cli.ts create mode 100644 src/common/api-key/api-key.module.ts create mode 100644 src/common/api-key/constants/api-key.constant.ts create mode 100644 src/common/api-key/constants/api-key.doc.ts create mode 100644 src/common/api-key/constants/api-key.list.constant.ts create mode 100644 src/common/api-key/constants/api-key.status-code.constant.ts create mode 100644 src/common/api-key/controllers/api-key.admin.controller.ts create mode 100644 src/common/api-key/controllers/api-key.controller.ts create mode 100644 src/common/api-key/decorators/api-key.admin.decorator.ts create mode 100644 src/common/api-key/decorators/api-key.decorator.ts create mode 100644 src/common/api-key/docs/api-key.admin.doc.ts create mode 100644 src/common/api-key/dtos/api-key.active.dto.ts create mode 100644 src/common/api-key/dtos/api-key.create.dto.ts create mode 100644 src/common/api-key/dtos/api-key.request.dto.ts create mode 100644 src/common/api-key/dtos/api-key.update-date.dto.ts create mode 100644 src/common/api-key/dtos/api-key.update.dto.ts create mode 100644 src/common/api-key/guards/api-key.active.guard.ts create mode 100644 src/common/api-key/guards/api-key.expired.guard.ts create mode 100644 src/common/api-key/guards/api-key.not-found.guard.ts create mode 100644 src/common/api-key/guards/api-key.put-to-request.guard.ts create mode 100644 src/common/api-key/guards/x-api-key/api-key.x-api-key.guard.ts create mode 100644 src/common/api-key/guards/x-api-key/api-key.x-api-key.strategy.ts create mode 100644 src/common/api-key/interfaces/api-key.interface.ts create mode 100644 src/common/api-key/interfaces/api-key.service.interface.ts create mode 100644 src/common/api-key/repository/api-key.repository.module.ts create mode 100644 src/common/api-key/repository/entities/api-key.entity.ts create mode 100644 src/common/api-key/repository/repositories/api-key.repository.ts create mode 100644 src/common/api-key/serializations/api-key.create.serialization.ts create mode 100644 src/common/api-key/serializations/api-key.get.serialization.ts create mode 100644 src/common/api-key/serializations/api-key.list.serialization.ts create mode 100644 src/common/api-key/serializations/api-key.reset.serialization.ts create mode 100644 src/common/api-key/services/api-key.service.ts create mode 100644 src/common/api-key/tasks/api-key.inactive.task.ts create mode 100644 src/common/auth/auth.module.ts create mode 100644 src/common/auth/constants/auth.constant.ts create mode 100644 src/common/auth/constants/auth.enum.constant.ts create mode 100644 src/common/auth/constants/auth.enum.permission.constant.ts create mode 100644 src/common/auth/constants/auth.status-code.constant.ts create mode 100644 src/common/auth/decorators/auth.jwt.decorator.ts create mode 100644 src/common/auth/decorators/auth.permission.decorator.ts create mode 100644 src/common/auth/guards/jwt-access/auth.jwt-access.guard.ts create mode 100644 src/common/auth/guards/jwt-access/auth.jwt-access.strategy.ts create mode 100644 src/common/auth/guards/jwt-refresh/auth.jwt-refresh.guard.ts create mode 100644 src/common/auth/guards/jwt-refresh/auth.jwt-refresh.strategy.ts create mode 100644 src/common/auth/guards/payload/auth.payload.access-for.guard.ts create mode 100644 src/common/auth/guards/payload/auth.payload.permission.guard.ts create mode 100644 src/common/auth/guards/permission/auth.permission.guard.ts create mode 100644 src/common/auth/interfaces/auth.interface.ts create mode 100644 src/common/auth/interfaces/auth.service.interface.ts create mode 100644 src/common/auth/services/auth.service.ts create mode 100644 src/common/aws/aws.module.ts create mode 100644 src/common/aws/constants/aws.s3.constant.ts create mode 100644 src/common/aws/interfaces/aws.interface.ts create mode 100644 src/common/aws/interfaces/aws.s3-service.interface.ts create mode 100644 src/common/aws/serializations/aws.s3-multipart.serialization.ts create mode 100644 src/common/aws/serializations/aws.s3.serialization.ts create mode 100644 src/common/aws/services/aws.s3.service.ts create mode 100644 src/common/common.module.ts create mode 100644 src/common/dashboard/constants/dashboard.doc.constant.ts create mode 100644 src/common/dashboard/dashboard.module.ts create mode 100644 src/common/dashboard/dtos/dashboard.ts create mode 100644 src/common/dashboard/interfaces/dashboard.interface.ts create mode 100644 src/common/dashboard/interfaces/dashboard.service.interface.ts create mode 100644 src/common/dashboard/serializations/dashboard.month-and-year.serialization.ts create mode 100644 src/common/dashboard/serializations/dashboard.serialization.ts create mode 100644 src/common/dashboard/services/dashboard.service.ts create mode 100644 src/common/database/abstracts/database.base-entity.abstract.ts create mode 100644 src/common/database/abstracts/database.base-repository.abstract.ts create mode 100644 src/common/database/abstracts/mongo/entities/database.mongo.object-id.entity.abstract.ts create mode 100644 src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract.ts create mode 100644 src/common/database/abstracts/mongo/repositories/database.mongo.object-id.repository.abstract.ts create mode 100644 src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract.ts create mode 100644 src/common/database/constants/database.constant.ts create mode 100644 src/common/database/constants/database.function.constant.ts create mode 100644 src/common/database/database.options.module.ts create mode 100644 src/common/database/decorators/database.decorator.ts create mode 100644 src/common/database/interfaces/database.interface.ts create mode 100644 src/common/database/interfaces/database.options-service.interface.ts create mode 100644 src/common/database/services/database.options.service.ts create mode 100644 src/common/debugger/constants/debugger.constant.ts create mode 100644 src/common/debugger/debugger.module.ts create mode 100644 src/common/debugger/debugger.options.module.ts create mode 100644 src/common/debugger/interfaces/debugger.interface.ts create mode 100644 src/common/debugger/interfaces/debugger.options-service.interface.ts create mode 100644 src/common/debugger/interfaces/debugger.service.interface.ts create mode 100644 src/common/debugger/middleware/debugger.middleware.module.ts create mode 100644 src/common/debugger/middleware/http/debugger.http.middleware.ts create mode 100644 src/common/debugger/services/debugger.options.service.ts create mode 100644 src/common/debugger/services/debugger.service.ts create mode 100644 src/common/doc/constants/doc.enum.constant.ts create mode 100644 src/common/doc/decorators/doc.decorator.ts create mode 100644 src/common/doc/interfaces/doc.interface.ts create mode 100644 src/common/error/constants/error.constant.ts create mode 100644 src/common/error/constants/error.enum.constant.ts create mode 100644 src/common/error/constants/error.status-code.constant.ts create mode 100644 src/common/error/decorators/error.decorator.ts create mode 100644 src/common/error/error.module.ts create mode 100644 src/common/error/filters/error.http.filter.ts create mode 100644 src/common/error/guards/error.meta.guard.ts create mode 100644 src/common/error/interfaces/error.interface.ts create mode 100644 src/common/error/serializations/error.serialization.ts create mode 100644 src/common/file/constants/file.constant.ts create mode 100644 src/common/file/constants/file.enum.constant.ts create mode 100644 src/common/file/constants/file.status-code.constant.ts create mode 100644 src/common/file/decorators/file.decorator.ts create mode 100644 src/common/file/dtos/file.multiple.dto.ts create mode 100644 src/common/file/dtos/file.single.dto.ts create mode 100644 src/common/file/interceptors/file.custom-max-files.interceptor.ts create mode 100644 src/common/file/interceptors/file.custom-max-size.interceptor.ts create mode 100644 src/common/file/interfaces/file.interface.ts create mode 100644 src/common/file/pipes/file.extract.pipe.ts create mode 100644 src/common/file/pipes/file.max-files.pipe.ts create mode 100644 src/common/file/pipes/file.required.pipe.ts create mode 100644 src/common/file/pipes/file.size.pipe.ts create mode 100644 src/common/file/pipes/file.type.pipe.ts create mode 100644 src/common/file/pipes/file.validation.pipe.ts create mode 100644 src/common/helper/constants/helper.enum.constant.ts create mode 100644 src/common/helper/constants/helper.function.constant.ts create mode 100644 src/common/helper/helper.module.ts create mode 100644 src/common/helper/interfaces/helper.array-service.interface.ts create mode 100644 src/common/helper/interfaces/helper.date-service.interface.ts create mode 100644 src/common/helper/interfaces/helper.encryption-service.interface.ts create mode 100644 src/common/helper/interfaces/helper.file-service.interface.ts create mode 100644 src/common/helper/interfaces/helper.geo-service.interface.ts create mode 100644 src/common/helper/interfaces/helper.hash-service.interface.ts create mode 100644 src/common/helper/interfaces/helper.interface.ts create mode 100644 src/common/helper/interfaces/helper.number-service.interface.ts create mode 100644 src/common/helper/interfaces/helper.string-service.interface.ts create mode 100644 src/common/helper/services/helper.array.service.ts create mode 100644 src/common/helper/services/helper.date.service.ts create mode 100644 src/common/helper/services/helper.encryption.service.ts create mode 100644 src/common/helper/services/helper.file.service.ts create mode 100644 src/common/helper/services/helper.geo.service.ts create mode 100644 src/common/helper/services/helper.hash.service.ts create mode 100644 src/common/helper/services/helper.number.service.ts create mode 100644 src/common/helper/services/helper.string.service.ts create mode 100644 src/common/logger/constants/logger.constant.ts create mode 100644 src/common/logger/constants/logger.enum.constant.ts create mode 100644 src/common/logger/decorators/logger.decorator.ts create mode 100644 src/common/logger/dtos/logger.create.dto.ts create mode 100644 src/common/logger/interceptors/logger.interceptor.ts create mode 100644 src/common/logger/interfaces/logger.interface.ts create mode 100644 src/common/logger/interfaces/logger.service.interface.ts create mode 100644 src/common/logger/logger.module.ts create mode 100644 src/common/logger/repository/entities/logger.entity.ts create mode 100644 src/common/logger/repository/logger.repository.module.ts create mode 100644 src/common/logger/repository/repositories/logger.repository.ts create mode 100644 src/common/logger/services/logger.service.ts create mode 100644 src/common/message/constants/message.enum.constant.ts create mode 100644 src/common/message/controllers/message.controller.ts create mode 100644 src/common/message/docs/message.enum.doc.ts create mode 100644 src/common/message/interfaces/message.interface.ts create mode 100644 src/common/message/interfaces/message.service.interface.ts create mode 100644 src/common/message/message.module.ts create mode 100644 src/common/message/middleware/custom-language/message.custom-language.middleware.ts create mode 100644 src/common/message/middleware/message.middleware.module.ts create mode 100644 src/common/message/serializations/message.language.serialization.ts create mode 100644 src/common/message/services/message.service.ts create mode 100644 src/common/pagination/constants/pagination.constant.ts create mode 100644 src/common/pagination/constants/pagination.enum.constant.ts create mode 100644 src/common/pagination/decorators/pagination.decorator.ts create mode 100644 src/common/pagination/dtos/pagination.list.dto.ts create mode 100644 src/common/pagination/interfaces/pagination.interface.ts create mode 100644 src/common/pagination/interfaces/pagination.service.interface.ts create mode 100644 src/common/pagination/pagination.module.ts create mode 100644 src/common/pagination/pipes/pagination.filter-contain.pipe.ts create mode 100644 src/common/pagination/pipes/pagination.filter-date.pipe.ts create mode 100644 src/common/pagination/pipes/pagination.filter-equal-object-id.pipe.ts create mode 100644 src/common/pagination/pipes/pagination.filter-equal.pipe.ts create mode 100644 src/common/pagination/pipes/pagination.filter-in-boolean.pipe.ts create mode 100644 src/common/pagination/pipes/pagination.filter-in-enum.pipe.ts create mode 100644 src/common/pagination/pipes/pagination.order.pipe.ts create mode 100644 src/common/pagination/pipes/pagination.paging.pipe.ts create mode 100644 src/common/pagination/pipes/pagination.search.pipe.ts create mode 100644 src/common/pagination/services/pagination.service.ts create mode 100644 src/common/request/constants/request.constant.ts create mode 100644 src/common/request/constants/request.enum.constant.ts create mode 100644 src/common/request/constants/request.status-code.constant.ts create mode 100644 src/common/request/decorators/request.decorator.ts create mode 100644 src/common/request/guards/request.param.guard.ts create mode 100644 src/common/request/interceptors/request.timeout.interceptor.ts create mode 100644 src/common/request/interceptors/request.timestamp.interceptor.ts create mode 100644 src/common/request/interceptors/request.user-agent.interceptor.ts create mode 100644 src/common/request/interfaces/request.interface.ts create mode 100644 src/common/request/middleware/body-parser/request.body-parser.middleware.ts create mode 100644 src/common/request/middleware/cors/request.cors.middleware.ts create mode 100644 src/common/request/middleware/helmet/request.helmet.middleware.ts create mode 100644 src/common/request/middleware/id/request.id.middleware.ts create mode 100644 src/common/request/middleware/request.middleware.module.ts create mode 100644 src/common/request/middleware/timestamp/request.timestamp.middleware.ts create mode 100644 src/common/request/middleware/timezone/request.timezone.middleware.ts create mode 100644 src/common/request/middleware/user-agent/request.user-agent.middleware.ts create mode 100644 src/common/request/middleware/version/request.version.middleware.ts create mode 100644 src/common/request/request.module.ts create mode 100644 src/common/request/serializations/request.pagination.serialization.ts create mode 100644 src/common/request/validations/request.is-password-medium.validation.ts create mode 100644 src/common/request/validations/request.is-password-strong.validation.ts create mode 100644 src/common/request/validations/request.is-password-weak.validation.ts create mode 100644 src/common/request/validations/request.is-start-with.validation.ts create mode 100644 src/common/request/validations/request.max-binary-file.validation.ts create mode 100644 src/common/request/validations/request.max-date-today.validation.ts create mode 100644 src/common/request/validations/request.max-greater-than-equal.validation.ts create mode 100644 src/common/request/validations/request.max-greater-than.validation.ts create mode 100644 src/common/request/validations/request.min-date-today.validation.ts create mode 100644 src/common/request/validations/request.min-greater-than-equal.validation.ts create mode 100644 src/common/request/validations/request.min-greater-than.validation.ts create mode 100644 src/common/request/validations/request.mobile-number-allowed.validation.ts create mode 100644 src/common/request/validations/request.only-digits.validation.ts create mode 100644 src/common/request/validations/request.safe-string.validation.ts create mode 100644 src/common/request/validations/request.skip.validation.ts create mode 100644 src/common/response/constants/response.constant.ts create mode 100644 src/common/response/decorators/response.decorator.ts create mode 100644 src/common/response/interceptors/response.custom-headers.interceptor.ts create mode 100644 src/common/response/interceptors/response.default.interceptor.ts create mode 100644 src/common/response/interceptors/response.excel.interceptor.ts create mode 100644 src/common/response/interceptors/response.paging.interceptor.ts create mode 100644 src/common/response/interfaces/response.interface.ts create mode 100644 src/common/response/middleware/response.middleware.module.ts create mode 100644 src/common/response/middleware/time/response.time.middleware.ts create mode 100644 src/common/response/response.module.ts create mode 100644 src/common/response/serializations/response.default.serialization.ts create mode 100644 src/common/response/serializations/response.id.serialization.ts create mode 100644 src/common/response/serializations/response.paging.serialization.ts create mode 100644 src/common/setting/constants/setting.doc.constant.ts create mode 100644 src/common/setting/constants/setting.enum.constant.ts create mode 100644 src/common/setting/constants/setting.list.constant.ts create mode 100644 src/common/setting/constants/setting.status-code.constant.ts create mode 100644 src/common/setting/controllers/setting.admin.controller.ts create mode 100644 src/common/setting/controllers/setting.controller.ts create mode 100644 src/common/setting/decorators/setting.admin.decorator.ts create mode 100644 src/common/setting/decorators/setting.decorator.ts create mode 100644 src/common/setting/docs/setting.admin.doc.ts create mode 100644 src/common/setting/docs/setting.doc.ts create mode 100644 src/common/setting/dtos/setting.create.dto.ts create mode 100644 src/common/setting/dtos/setting.request.dto.ts create mode 100644 src/common/setting/dtos/setting.update-value.dto.ts create mode 100644 src/common/setting/guards/setting.not-found.guard.ts create mode 100644 src/common/setting/guards/setting.put-to-request.guard.ts create mode 100644 src/common/setting/interfaces/setting.service.interface.ts create mode 100644 src/common/setting/middleware/maintenance/setting.maintenance.middleware.ts create mode 100644 src/common/setting/middleware/setting.middleware.module.ts create mode 100644 src/common/setting/repository/entities/setting.entity.ts create mode 100644 src/common/setting/repository/repositories/setting.repository.ts create mode 100644 src/common/setting/repository/setting.repository.module.ts create mode 100644 src/common/setting/serializations/setting.get.serialization.ts create mode 100644 src/common/setting/serializations/setting.list.serialization.ts create mode 100644 src/common/setting/services/setting.service.ts create mode 100644 src/common/setting/setting.module.ts create mode 100644 src/configs/app.config.ts create mode 100644 src/configs/auth.config.ts create mode 100644 src/configs/aws.config.ts create mode 100644 src/configs/database.config.ts create mode 100644 src/configs/debugger.config.ts create mode 100644 src/configs/doc.config.ts create mode 100644 src/configs/file.config.ts create mode 100644 src/configs/helper.config.ts create mode 100644 src/configs/index.ts create mode 100644 src/configs/request.config.ts create mode 100644 src/configs/user.config.ts create mode 100644 src/health/controllers/health.controller.ts create mode 100644 src/health/docs/health.doc.ts create mode 100644 src/health/health.module.ts create mode 100644 src/health/indicators/health.aws-s3.indicator.ts create mode 100644 src/health/serializations/health.serialization.ts create mode 100644 src/jobs/jobs.module.ts create mode 100644 src/jobs/router/jobs.router.module.ts create mode 100644 src/languages/en/apiKey.json create mode 100644 src/languages/en/app.json create mode 100644 src/languages/en/auth.json create mode 100644 src/languages/en/file.json create mode 100644 src/languages/en/health.json create mode 100644 src/languages/en/http.json create mode 100644 src/languages/en/message.json create mode 100644 src/languages/en/middleware.json create mode 100644 src/languages/en/permission.json create mode 100644 src/languages/en/request.json create mode 100644 src/languages/en/role.json create mode 100644 src/languages/en/setting.json create mode 100644 src/languages/en/user.json create mode 100644 src/languages/id/app.json create mode 100644 src/languages/id/request.json create mode 100644 src/main.ts create mode 100644 src/migration/migration.module.ts create mode 100644 src/migration/seeds/migration.api-key.seed.ts create mode 100644 src/migration/seeds/migration.permission.seed.ts create mode 100644 src/migration/seeds/migration.role.seed.ts create mode 100644 src/migration/seeds/migration.setting.seed.ts create mode 100644 src/migration/seeds/migration.user.seed.ts create mode 100644 src/modules/permission/constants/permission.constant.ts create mode 100644 src/modules/permission/constants/permission.doc.constant.ts create mode 100644 src/modules/permission/constants/permission.enum.constant.ts create mode 100644 src/modules/permission/constants/permission.list.constant.ts create mode 100644 src/modules/permission/constants/permission.status-code.constant.ts create mode 100644 src/modules/permission/controllers/permission.admin.controller.ts create mode 100644 src/modules/permission/decorators/permission.admin.decorator.ts create mode 100644 src/modules/permission/decorators/permission.decorator.ts create mode 100644 src/modules/permission/docs/permission.admin.doc.ts create mode 100644 src/modules/permission/dtos/permission.active.dto.ts create mode 100644 src/modules/permission/dtos/permission.create.dto.ts create mode 100644 src/modules/permission/dtos/permission.update-description.dto.ts create mode 100644 src/modules/permission/dtos/permission.update-group.dto.ts create mode 100644 src/modules/permission/dtos/permissions.request.dto.ts create mode 100644 src/modules/permission/guards/permission.active.guard.ts create mode 100644 src/modules/permission/guards/permission.not-found.guard.ts create mode 100644 src/modules/permission/guards/permission.put-to-request.guard.ts create mode 100644 src/modules/permission/interfaces/permission.interface.ts create mode 100644 src/modules/permission/interfaces/permission.service.interface.ts create mode 100644 src/modules/permission/permission.module.ts create mode 100644 src/modules/permission/repository/entities/permission.entity.ts create mode 100644 src/modules/permission/repository/permission.repository.module.ts create mode 100644 src/modules/permission/repository/repositories/permission.repository.ts create mode 100644 src/modules/permission/serializations/permission.get.serialization.ts create mode 100644 src/modules/permission/serializations/permission.group.serialization.ts create mode 100644 src/modules/permission/serializations/permission.list.serialization.ts create mode 100644 src/modules/permission/services/permission.service.ts create mode 100644 src/modules/role/constants/role.constant.ts create mode 100644 src/modules/role/constants/role.doc.constant.ts create mode 100644 src/modules/role/constants/role.list.constant.ts create mode 100644 src/modules/role/constants/role.status-code.constant.ts create mode 100644 src/modules/role/controllers/role.admin.controller.ts create mode 100644 src/modules/role/decorators/role.admin.decorator.ts create mode 100644 src/modules/role/decorators/role.decorator.ts create mode 100644 src/modules/role/docs/role.admin.doc.ts create mode 100644 src/modules/role/dtos/role.active.dto.ts create mode 100644 src/modules/role/dtos/role.create.dto.ts create mode 100644 src/modules/role/dtos/role.request.dto.ts create mode 100644 src/modules/role/dtos/role.update-name.dto.ts create mode 100644 src/modules/role/dtos/role.update-permission.dto.ts create mode 100644 src/modules/role/guards/role.active.guard.ts create mode 100644 src/modules/role/guards/role.not-found.guard.ts create mode 100644 src/modules/role/guards/role.put-to-request.guard.ts create mode 100644 src/modules/role/guards/role.used.guard.ts create mode 100644 src/modules/role/interfaces/role.interface.ts create mode 100644 src/modules/role/interfaces/role.service.interface.ts create mode 100644 src/modules/role/repository/entities/role.entity.ts create mode 100644 src/modules/role/repository/repositories/role.repository.ts create mode 100644 src/modules/role/repository/role.repository.module.ts create mode 100644 src/modules/role/role.module.ts create mode 100644 src/modules/role/serializations/role.access-for.serialization.ts create mode 100644 src/modules/role/serializations/role.get.serialization.ts create mode 100644 src/modules/role/serializations/role.list.serialization.ts create mode 100644 src/modules/role/services/role.service.ts create mode 100644 src/modules/user/constants/user.constant.ts create mode 100644 src/modules/user/constants/user.doc.constant.ts create mode 100644 src/modules/user/constants/user.list.constant.ts create mode 100644 src/modules/user/constants/user.status-code.constant.ts create mode 100644 src/modules/user/controllers/user.admin.controller.ts create mode 100644 src/modules/user/controllers/user.controller.ts create mode 100644 src/modules/user/controllers/user.public.controller.ts create mode 100644 src/modules/user/decorators/user.admin.decorator.ts create mode 100644 src/modules/user/decorators/user.decorator.ts create mode 100644 src/modules/user/decorators/user.public.decorator.ts create mode 100644 src/modules/user/docs/user.admin.doc.ts create mode 100644 src/modules/user/docs/user.doc.ts create mode 100644 src/modules/user/docs/user.public.doc.ts create mode 100644 src/modules/user/dtos/user.active.dto.ts create mode 100644 src/modules/user/dtos/user.block.dto.ts create mode 100644 src/modules/user/dtos/user.change-password.dto.ts create mode 100644 src/modules/user/dtos/user.create.dto.ts create mode 100644 src/modules/user/dtos/user.grant-permission.dto.ts create mode 100644 src/modules/user/dtos/user.import.dto.ts create mode 100644 src/modules/user/dtos/user.login.dto.ts create mode 100644 src/modules/user/dtos/user.password-attempt.dto.ts create mode 100644 src/modules/user/dtos/user.password-expired.dto.ts create mode 100644 src/modules/user/dtos/user.password.dto.ts create mode 100644 src/modules/user/dtos/user.photo.dto.ts create mode 100644 src/modules/user/dtos/user.request.dto.ts create mode 100644 src/modules/user/dtos/user.sign-up.dto.ts create mode 100644 src/modules/user/dtos/user.update-name.dto.ts create mode 100644 src/modules/user/guards/payload/user.payload.put-to-request.guard.ts create mode 100644 src/modules/user/guards/user.active.guard.ts create mode 100644 src/modules/user/guards/user.blocked.guard.ts create mode 100644 src/modules/user/guards/user.not-found.guard.ts create mode 100644 src/modules/user/guards/user.put-to-request.guard.ts create mode 100644 src/modules/user/interfaces/user.interface.ts create mode 100644 src/modules/user/interfaces/user.service.interface.ts create mode 100644 src/modules/user/repository/entities/user.entity.ts create mode 100644 src/modules/user/repository/repositories/user.repository.ts create mode 100644 src/modules/user/repository/user.repository.module.ts create mode 100644 src/modules/user/serializations/user.get.serialization.ts create mode 100644 src/modules/user/serializations/user.grant-permission.serialization.ts create mode 100644 src/modules/user/serializations/user.import.serialization.ts create mode 100644 src/modules/user/serializations/user.info.serialization.ts create mode 100644 src/modules/user/serializations/user.list.serialization.ts create mode 100644 src/modules/user/serializations/user.login.serialization.ts create mode 100644 src/modules/user/serializations/user.payload-permission.serialization.ts create mode 100644 src/modules/user/serializations/user.payload.serialization.ts create mode 100644 src/modules/user/serializations/user.profile.serialization.ts create mode 100644 src/modules/user/services/user.service.ts create mode 100644 src/modules/user/user.module.ts create mode 100644 src/router/router.module.ts create mode 100644 src/router/routes/routes.admin.module.ts create mode 100644 src/router/routes/routes.callback.module.ts create mode 100644 src/router/routes/routes.module.ts create mode 100644 src/router/routes/routes.public.module.ts create mode 100644 src/router/routes/routes.test.module.ts create mode 100644 src/swagger.ts create mode 100644 test/e2e/api-key/api-key.admin.e2e-spec.ts create mode 100644 test/e2e/api-key/api-key.constant.ts create mode 100644 test/e2e/api-key/api-key.e2e-spec.ts create mode 100644 test/e2e/jest.json create mode 100644 test/e2e/permission/permission.admin.e2e-spec.ts create mode 100644 test/e2e/permission/permission.constant.ts create mode 100644 test/e2e/role/role.admin.e2e-spec.ts create mode 100644 test/e2e/role/role.constant.ts create mode 100644 test/e2e/setting/setting.admin.e2e-spec.ts create mode 100644 test/e2e/setting/setting.constant.ts create mode 100644 test/e2e/setting/setting.e2e-spec.ts create mode 100644 test/e2e/user/files/import.csv create mode 100644 test/e2e/user/files/medium.jpg create mode 100644 test/e2e/user/files/small.jpg create mode 100644 test/e2e/user/files/test.txt create mode 100644 test/e2e/user/user.admin.e2e-spec.ts create mode 100644 test/e2e/user/user.change-password.e2e-spec.ts create mode 100644 test/e2e/user/user.constant.ts create mode 100644 test/e2e/user/user.e2e-spec.ts create mode 100644 test/e2e/user/user.grant-permission.e2e-spec.ts create mode 100644 test/e2e/user/user.info.e2e-spec.ts create mode 100644 test/e2e/user/user.login.e2e-spec.ts create mode 100644 test/e2e/user/user.public.e2e-spec.ts create mode 100644 test/e2e/user/user.refresh.e2e-spec.ts create mode 100644 test/integration/aws/aws.s3.constant.ts create mode 100644 test/integration/aws/aws.s3.integration.spec.ts create mode 100644 test/integration/database/database.constant.ts create mode 100644 test/integration/database/database.integration.spec.ts create mode 100644 test/integration/jest.json create mode 100644 test/unit/api-key/api-key.service.spec.ts create mode 100644 test/unit/auth/auth.service.spec.ts create mode 100644 test/unit/database/database.options.service.spec.ts create mode 100644 test/unit/debugger/debugger.options.service.spec.ts create mode 100644 test/unit/debugger/debugger.service.spec.ts create mode 100644 test/unit/helper/helper.array.service.spec.ts create mode 100644 test/unit/helper/helper.date.service.spec.ts create mode 100644 test/unit/helper/helper.encryption.service.spec.ts create mode 100644 test/unit/helper/helper.file.service.spec.ts create mode 100644 test/unit/helper/helper.geo.service.spec.ts create mode 100644 test/unit/helper/helper.hash.service.spec.ts create mode 100644 test/unit/helper/helper.number.service.spec.ts create mode 100644 test/unit/helper/helper.string.service.spec.ts create mode 100644 test/unit/jest.json create mode 100644 test/unit/logger/logger.service.spec.ts create mode 100644 test/unit/message/message.service.spec.ts create mode 100644 test/unit/pagination/pagination.service.spec.ts create mode 100644 test/unit/setting/setting.service.spec.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..aba9ac8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,31 @@ +#husky + +# Build dependencies +node_modules/ +coverage/ +coverage-e2e/ +coverage-integration/ +dist/ +.husky/ +.github/ +prod/ + +# Logs +logs/ + +# Environment (contains sensitive data) +.env* + +# Versioning and metadata +.git +.gitignore +.dockerignore +.eslintignore + +# Files not required for production +.editorconfig +dockerfile +docker-compose.yml +cspell.json +README.md +nodemon.json \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e0a6456 --- /dev/null +++ b/.env.example @@ -0,0 +1,52 @@ +APP_NAME=NEST +APP_ENV=development +APP_LANGUAGE=en + +HTTP_ENABLE=true +HTTP_HOST=localhost +HTTP_PORT= 3000 +HTTP_VERSIONING_ENABLE=true +HTTP_VERSION=1 + +DEBUGGER_HTTP_WRITE_INTO_FILE=false +DEBUGGER_HTTP_WRITE_INTO_CONSOLE=false +DEBUGGER_SYSTEM_WRITE_INTO_FILE=false +DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE=false + +JOB_ENABLE=false + +DATABASE_HOST=mongodb://localhost:30001,localhost:30002,localhost:30003 +DATABASE_NAME=nest +DATABASE_USER= +DATABASE_PASSWORD= +DATABASE_DEBUG=false +DATABASE_OPTIONS=replicaSet=rs0&retryWrites=true&w=majority + +AUTH_JWT_SUBJECT=nestDevelopment +AUTH_JWT_ISSUER=nest +AUTH_JWT_AUDIENCE=https://example.com + +AUTH_JWT_ACCESS_TOKEN_SECRET_KEY=85huyujDurLdvLsjAW93XsqP79rAotqplHCOEWj1wzyIcMtT +AUTH_JWT_ACCESS_TOKEN_EXPIRED=15m + +AUTH_JWT_REFRESH_TOKEN_SECRET_KEY=7Y3nqaO8jKVOFBRy9ujn5uUxV8Iy2otHrnQgiXlIGAqiVdb5 +AUTH_JWT_REFRESH_TOKEN_EXPIRED=7d +AUTH_JWT_REFRESH_TOKEN_REMEMBER_ME_EXPIRED=30d +AUTH_JWT_REFRESH_TOKEN_NOT_BEFORE_EXPIRATION=15m + +AUTH_PERMISSION_TOKEN_SECRET_KEY=85huyujDurLdvLsjAW93XsqP79rAotqplHCOEWj1wzyIcMtT +AUTH_PERMISSION_TOKEN_EXPIRED=5m + +AUTH_JWT_PAYLOAD_ENCRYPT=false +AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_KEY=fKyRq7g9eftVNEdiCC7lNU6fga5Pr1iC7dc0JYsC +AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_IV=mLZZdQrXqjPW5F5H2eko +AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_KEY=NnCnSrRmw5YuQyTPtDokOWmKR37EYbuB6ITZqqZd +AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_IV=eP7P8Pmvq207zhyd61dz + +AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_KEY=hUcRIUQzJMe17w8cAZAreMdjxjo1JbucBACu7tAw +AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_IV=7V0D5a0D3SdsgM1KT5rF + +AWS_CREDENTIAL_KEY= +AWS_CREDENTIAL_SECRET= +AWS_S3_REGION=ap-southeast-3 +AWS_S3_BUCKET=baibay-development \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..0502376 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,9 @@ +# /node_modules/* in the project root is ignored by default +# build artefacts +dist/* +coverage/* +node_modules/* +logs/* +prod/* +.husky/* +.github/* diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..a951aaa --- /dev/null +++ b/.eslintrc @@ -0,0 +1,23 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "tsconfig.json", + "tsconfigRootDir": ".", + "sourceType": "module" + }, + "plugins": ["@typescript-eslint/eslint-plugin"], + "extends": [ + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "root": true, + "env": { + "node": true, + "jest": true + }, + "ignorePatterns": [".eslintrc.js"], + "rules": { + "@typescript-eslint/no-explicit-any": "off" + } +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9f73384 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: tuesday + time: "00:00" + open-pull-requests-limit: 3 + target-branch: "development" + commit-message: + prefix: "github-action" + labels: + - dependabot diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..bfe6046 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,31 @@ +name: Linter +on: + + workflow_dispatch: + pull_request: + branches: + - master + - development + +jobs: + linter: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: ['18.x'] + + steps: + - name: Git checkout + uses: actions/checkout@v4 + + - name: Setup node version ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: yarn --frozen-lockfile + + - name: Linter + run: yarn lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ded86a1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,45 @@ +name: Release + +permissions: + contents: write + +on: + + # push: + # branches: + # - main + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Git checkout + uses: actions/checkout@v4 + + - name: Get short sha commit + id: git + run: | + echo "short_sha=$(git rev-parse --short $GITHUB_SHA)" >> "$GITHUB_OUTPUT" + + - name: Get latest version + id: version + uses: martinbeentjes/npm-get-version-action@main + + - name: Git + run: | + echo Branch name is: ${{ github.ref_name }} + echo Short sha: ${{ steps.git.outputs.short_sha }} + echo Version is: ${{ steps.version.outputs.current-version }} + + - name: Release + uses: softprops/action-gh-release@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.version.outputs.current-version }} + name: v${{ steps.version.outputs.current-version }} + generate_release_notes: true + draft: false + prerelease: false diff --git a/.gitignore b/.gitignore index cab85ca..95b81ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,53 @@ +# compiled output +/dist /node_modules -/coverage +.warmup/ + +# Logs +logs *.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS .DS_Store + +# Tests +/coverage +/coverage-e2e +/coverage-integration +/.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 + +# yarn +.yarn/* +.pnp.* +!.yarn/cache +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# environment +.env* +!.env.example +config.yaml +config.yml \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..15e0bfa --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,6 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint +yarn deadcode +yarn spell diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 4c9adda..0000000 --- a/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -./docs -./scripts -./tests -./incoming \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2d6310f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "es5", + "tabWidth": 4 +} \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index b0e20f5..0000000 --- a/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -Copyright (c) All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..d152665 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) [2023] [Fedi DAYEG] + +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 index dc27f0f..c5a8af0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,514 @@ -# osr-package-template +[![Contributors][nest-contributors-shield]][nest-contributors] +[![Forks][nest-forks-shield]][nest-forks] +[![Stargazers][nest-stars-shield]][nest-stars] +[![Issues][nest-issues-shield]][nest-issues] +[![MIT License][nest-license-shield]][license] -Package basics \ No newline at end of file +[![NestJs][nestjs-shield]][ref-nestjs] +[![NodeJs][nodejs-shield]][ref-nodejs] +[![Typescript][typescript-shield]][ref-typescript] +[![MongoDB][mongodb-shield]][ref-mongodb] +[![JWT][jwt-shield]][ref-jwt] +[![Jest][jest-shield]][ref-jest] +[![Yarn][yarn-shield]][ref-yarn] +[![Docker][docker-shield]][ref-docker] + +# Complete NestJs Boilerplate 🔥 🚀 + +> This repo will representative of authentication service and authorization service + +[Complete NestJs][nest] is a [Http NestJs v9.x][ref-nestjs] boilerplate. Best uses for backend service. + +*You can [request feature][nest-issues] or [report bug][nest-issues] with following this link* + +## Table of contents + +* [Important](#important) +* [Next Todo](#next-todo) +* [Build With](#build-with) +* [Objective](#objective) +* [Features](#features) +* [Structure](#structure) + * [Folder Structure](#folder-structure) + * [Module Structure](#module-structure) + * [Response Structure](#response-structure) +* [Prerequisites](#prerequisites) +* [Getting Started](#getting-started) + * [Clone Repo](#clone-repo) + * [Install Dependencies](#install-dependencies) + * [Create environment](#create-environment) + * [Database Migration](#database-migration) + * [Test](#test) + * [Run Project](#run-project) + * [Run Project with Docker](#run-project-with-docker) +* [API Reference](#api-reference) +* [Environment](#environment) +* [Api Key Encryption](#api-key-encryption) +* [Adjust Mongoose Setting](#adjust-mongoose-setting) +* [License](#license) +* [Contact](#contact) + +## Important + +* The features will replated with AWS Features +* If you want to implement `database transactions`, you must run MongoDB as a `replication set`. +* If you change the environment value of `APP_ENV` to `production`, that will trigger. + 1. CorsMiddleware will implement `src/configs/middleware.config.ts`. + 2. Documentation will `disable`. + +## Next Todo + +Next development + +* [x] serialization update +* [x] update unit test for common modules +* [x] user e2e +* [x] api key for saas, change `x-api-key` to `${key}:${secret}` +* [ ] user plan module +* [ ] invoicing plan module +* [ ] excel decorator optimize +* [ ] Authorization optimize, remove permission entity and implement policy guard +* [ ] Google SSO +* [ ] Background export/import from/to CSV and Excel +* [ ] Update Documentation, include an diagram for easier comprehension + +## Build with + +Describes which version. + +| Name | Version | +| ---------- | -------- | +| NestJs | v9.3.x | +| NodeJs | v18.12.x | +| Typescript | v5.0.x | +| Mongoose | v7.0.x | +| MongoDB | v6.0.x | +| Yarn | v1.22.x | +| NPM | v8.19.x | +| Docker | v20.10.x | +| Docker Compose | v2.6.x | +| Swagger | v6.2.x | + +## Objective + +* Easy to maintenance +* NestJs Habit +* Component based folder structure +* Stateless authentication and authorization +* Repository Design Pattern or Data Access Layer Design Pattern +* Follow Community Guide Line +* Follow The Twelve-Factor App +* Adopt SOLID and KISS principle +* Support Microservice Architecture, Serverless Architecture, Clean Architecture, and/or Hexagonal Architecture + +## Features + +### Main Features + +* NestJs v9.x 🥳 +* Typescript 🚀 +* Production ready 🔥 +* Repository Design Pattern (Multi Repository, can mix with `TypeORM`) +* Swagger / OpenAPI 3 included +* Authentication (`Access Token`, `Refresh Token`, `API Key`, and `Google SSO`) +* Authorization, Role and Permission Management (`PermissionToken`) +* Support multi-language `i18n` 🗣, can controllable with request header `x-custom-lang` +* Request validation for all request params, query, dan body with `class-validation` +* Serialization with `class-transformer` +* Url Versioning, default version is `1` +* Server Side Pagination +* Import and export data with CSV or Excel by using `decorator` + +### Database + +* MongoDB integrate by using [mongoose][ref-mongoose] 🎉 +* Multi Database +* Database Transaction +* Database Soft Delete +* Database Migration + +### Logger and Debugger + +* Logger with `Morgan` +* Debugger with `Winston` 📝 + +### Security + +* Apply `helmet`, `cors`, and `rate-limit` +* Timeout awareness and can override ⌛️ +* User agent awareness, and can whitelist user agent + +### Setting + +* Support environment file +* Centralize configuration 🤖 +* Centralize response +* Centralize exception filter +* Setting from database 🗿 + +### Third Party Integration + +* Storage integration with `AwsS3` +* Upload file `single` and `multipart` to AwsS3 + +### Others + +* Support Docker installation +* Support CI/CD with Github Action or Jenkins +* Husky GitHook for check source code, and run test before commit 🐶 +* Linter with EsLint for Typescript + +## Structure + +### Folder Structure + +1. `/app` The final wrapper module +2. `/common` The common module +3. `/configs` The configurations for this project +4. `/health` health check module for every service integrated +5. `/jobs` cron job or schedule task +6. `/language` json languages +7. `/migration` migrate all init data +8. `/modules` other modules based on service based on project +9. `/router` endpoint router. `Controller` will put in this + +### Module structure + +Full structure of module + +```txt +. +└── module1 + ├── abstracts + ├── constants // constant like enum, static value, status code, etc + ├── controllers // business logic for rest api + ├── decorators // warper decorator, custom decorator, etc + ├── dtos // request validation + ├── docs // swagger / OpenAPI 3 + ├── errors // custom error + ├── filters // custom filter + ├── guards // validate related with database + ├── indicators // custom health check indicator + ├── interceptors // custom interceptors + ├── interfaces + ├── middleware + ├── pipes + ├── repository + ├── entities // database entities + ├── repositories // database repositories + └── module1.repository.module.ts + ├── serializations // response serialization + ├── services + ├── tasks // task for cron job + └── module1.module.ts +``` + +### Response Structure + +This section will describe the structure of the response. + +#### Response Default + +Default response for the response + +```ts +export class ResponseDefaultSerialization { + statusCode: number; + message: string; + _metadata?: IResponseMetadata; + data?: Record; +} +``` + +#### Response Paging + +Default response for pagination. + +```ts +export class ResponsePagingSerialization { + statusCode: number; + message: string; + totalData: number; + totalPage?: number; + currentPage?: number; + perPage?: number; + _availableSearch?: string[]; + _availableSort?: string[]; + _metadata?: IResponseMetadata; + data: Record[]; +} + +``` + +#### Response Metadata + +This is useful when we need to give the frontend some information that is related / not related with the endpoint. + +```ts +export interface IResponseMetadata { + languages: ENUM_MESSAGE_LANGUAGE[]; + timestamp: number; + timezone: string; + requestId: string; + path: string; + version: string; + repoVersion: string; + nextPage?: string; + previousPage?: string; + firstPage?: string; + lastPage?: string; + [key: string]: any; +} +``` + +## Prerequisites + +We assume that everyone who comes here is **`programmer with intermediate knowledge`** and we also need to understand more before we begin in order to reduce the knowledge gap. + +1. Understand [NestJs Fundamental][ref-nestjs], Main Framework. NodeJs Framework with support fully TypeScript. +2. Understand[Typescript Fundamental][ref-typescript], Programming Language. It will help us to write and read the code. +3. Understand [ExpressJs Fundamental][ref-nodejs], NodeJs Base Framework. It will help us in understanding how the NestJs Framework works. +4. Understand what NoSql is and how it works as a database, especially [MongoDB.][ref-mongodb] +5. Understand Repository Design Pattern or Data Access Object Design Pattern. It will help to read, and write the source code +6. Understand The SOLID Principle and KISS Principle for better write the code. +7. Optional. Understand Microservice Architecture, Clean Architecture, and/or Hexagonal Architecture. It can help to serve the project. +8. Optional. Understanding [The Twelve Factor Apps][ref-12factor]. It can help to serve the project. +9. Optional. Understanding [Docker][ref-docker]. It can help to run the project. + +## Getting Started + +Before start, we need to install some packages and tools. +The recommended version is the LTS version for every tool and package. + +> Make sure to check that the tools have been installed successfully. + +1. [NodeJs][ref-nodejs] +2. [MongoDB][ref-mongodb] +3. [Yarn][ref-yarn] +4. [Git][ref-git] + +### Clone Repo + +Clone the project with git. + +```bash +git clone https://github.com/fedi-dayeg/complete-nestjs-boilerplate.git +``` + +### Install Dependencies + +This project needs some dependencies. Let's go install it. + +```bash +yarn install +``` + +### Create environment + +Make your own environment file with a copy of `env.example` and adjust values to suit your own environment. + +```bash +cp .env.example .env +``` + +To know the details, you can read the documentation. [Jump to document section](#documentation) + +### Database Migration + +> The migration will do data seeding to MongoDB. Make sure to check the value of the `DATABASE_` prefix in your`.env` file. + +The Database migration used [NestJs-Command][ref-nestjscommand] + +For seeding + +```bash +yarn seed +``` + +For remove all data do + +```bash +yarn rollback +``` + +### Test + +> The test is still not good net. I'm still lazy too do that. + +The project provide 3 automation testing `unit testing`, `integration testing`, and `e2e testing`. + +```bash +yarn test +``` + +For specific test do this + +* Unit testing + + ```bash + yarn test:unit + ``` + +* Integration testing + + ```bash + yarn test:integration + ``` + +* E2E testing + + ```bash + yarn test:e2e + ``` + +### Run Project + +Finally, Cheers 🍻🍻 !!! you passed all steps. + +Now you can run the project. + +```bash +yarn start:dev +``` + +### Run Project with Docker + +For docker installation, we need more tools to be installed in our instance. + +1. [Docker][ref-docker] +2. [Docker-Compose][ref-dockercompose] + +Then run + +```bash +docker-compose up -d +``` + +## API Reference + +You can check The ApiSpec after running this project. [here][api-reference-docs] + +## Documentation + +> Ongoing update + +## Adjust Mongoose Setting + +> Optional, if your mongodb version is < 5 + +Go to file `src/common/database/services/database.options.service.ts` and add `useMongoClient` to `mongooseOptions` then set value to `true`. + +```typescript +const mongooseOptions: MongooseModuleOptions = { + uri, + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + useMongoClient: true // <--- add this +}; +``` + +## License + +Distributed under [MIT licensed][license]. + +## Contribute + +How to contribute in this repo + +1. Fork the project with click `Fork` button of this repo. +2. Clone the fork project + + ```bash + git clone "url you just copied" + ``` + +3. Make necessary changes and commit those changes +4. Commit the changes + + ```bash + git commit -m "your message" + ``` + +5. Push changes to fork project + + ```bash + git push origin -u main + ``` + +6. Back to browser, goto your fork repo github. Then, click `Compare & pull request` + +If your code behind commit with the original, please update your code and resolve the conflict. Then, repeat from number 6. + +### Rule + +* Avoid Circular Dependency +* Consume component folder structure, and repository design pattern +* Always make `service` for every module is independently. +* Do not put `controller` into service modules, cause this will break the dependency. Only put the controller into `router` and then inject the dependency. +* Put the config in `/configs` folder, and for dynamic config put as `environment variable` +* `CommonModule` only for main package, and put the module that related of service/project into `/src/modules`. So, if we want to clear the unnecessary module, we just need to delete the `src/modules/**` +* If there a new service in CommonModule. Make sure to create the unit test in `/test/unit`. +* If there a new controller, make sure to create the e2e testing in `test/e2e` + +## Contact + +[Fedi DAYEG][author-email] + +[![Github][github-shield]][author-github] +[![LinkedIn][linkedin-shield]][author-linkedin] + + +[nest-contributors-shield]: https://img.shields.io/github/contributors/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge +[nest-forks-shield]: https://img.shields.io/github/forks/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge +[nest-stars-shield]: https://img.shields.io/github/stars/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge +[nest-issues-shield]: https://img.shields.io/github/issues/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge +[nest-license-shield]: https://img.shields.io/github/license/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge + +[nestjs-shield]: https://img.shields.io/badge/nestjs-%23E0234E.svg?style=for-the-badge&logo=nestjs&logoColor=white +[nodejs-shield]: https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white +[typescript-shield]: https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white +[mongodb-shield]: https://img.shields.io/badge/MongoDB-white?style=for-the-badge&logo=mongodb&logoColor=4EA94B +[jwt-shield]: https://img.shields.io/badge/JWT-000000?style=for-the-badge&logo=JSON%20web%20tokens&logoColor=white +[jest-shield]: https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white +[yarn-shield]: https://img.shields.io/badge/yarn-%232C8EBB.svg?style=for-the-badge&logo=yarn&logoColor=white +[docker-shield]: https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white + +[github-shield]: https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white +[linkedin-shield]: https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white + + +[author-linkedin]: https://www.linkedin.com/in/fedi-dayeg-a330b6131/ +[author-email]: mailto:fedi.dayeg@gmail.com +[author-github]: https://github.com/fedi-dayeg + + +[nest-issues]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate/issues +[nest-stars]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate/stargazers +[nest-forks]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate/network/members +[nest-contributors]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate/graphs/contributors + + +[nest]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate + + + +[license]: LICENSE.md + + +[ref-nestjs]: http://nestjs.com +[ref-mongoose]: https://mongoosejs.com +[ref-mongodb]: https://docs.mongodb.com/ +[ref-nodejs]: https://nodejs.org/ +[ref-typescript]: https://www.typescriptlang.org/ +[ref-docker]: https://docs.docker.com +[ref-dockercompose]: https://docs.docker.com/compose/ +[ref-yarn]: https://yarnpkg.com +[ref-12factor]: https://12factor.net +[ref-nestjscommand]: https://gitlab.com/aa900031/nestjs-command +[ref-jwt]: https://jwt.io +[ref-jest]: https://jestjs.io/docs/getting-started +[ref-git]: https://git-scm.com + + +[api-reference-docs]: http://localhost:3000/docs diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..3b49d9b --- /dev/null +++ b/cspell.json @@ -0,0 +1,60 @@ +{ + "version": "0.2", + "language": "en", + "words": [ + "nestjs", + "metatype", + "virtuals", + "prebuild", + "logform", + "transfomer", + "insync", + "microservices", + "globby", + "dockerhub", + "fifsky", + "buildx", + "exceljs", + "milis", + "workdir", + "dbdata", + "initdb", + "deadcode", + "authapis", + "headerapikey", + "jenkinsfile", + "superadmin", + "alphanum", + "dtos", + "typeorm", + "apikeys", + "apikey", + "maxlength", + "Streamable", + "unallow", + "datauser" + ], + "ignoreWords": [ + "psheon", + "aallithioo", + "tiaamoo", + "qwertyuiop12345zxcvbnmkjh", + "opbUwdiS1FBsrDUoPgZdx", + "cuwakimacojulawu", + "baibay", + "acks", + "andrechristikan", + "trueaaa", + "aasdasd" + ], + "ignorePaths": [ + "node_modules/**", + "endpoints/**", + "*coverage/**", + ".husky/**", + ".github/**", + "dist/**", + "logs/**", + "src/database/json/**" + ] +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..21bbf3d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.8' +services: + nestService: + build: . + container_name: nestService + hostname: nestService + ports: + - 3000:3000 + networks: + - app-network + volumes: + - ./src/:/app/src/ + - .env/:/app/.env + restart: unless-stopped + depends_on: + - nestDatabase + nestDatabase: + image: mongo:latest + container_name: nestDatabase + hostname: nestDatabase + ports: + - 27017:27017 + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: 123456 + MONGO_INITDB_DATABASE: nest + volumes: + - dbdata:/data/db + restart: unless-stopped + networks: + - app-network +networks: + app-network: + name: app-network + driver: bridge +volumes: + dbdata: \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..742e2bb --- /dev/null +++ b/dockerfile @@ -0,0 +1,14 @@ +FROM node:lts-alpine +LABEL maintainer "fedi.dayeg@gmail.com" + +WORKDIR /app +EXPOSE 3000 + +COPY package.json yarn.lock ./ +RUN touch .env + +RUN set -x && yarn + +COPY . . + +CMD [ "yarn", "start:dev" ] diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 0000000..e7ad649 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,16 @@ +{ + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "plugins": ["@nestjs/swagger"], + "assets": [ + { + "include": "languages/**/*", + "outDir": "dist/src" + } + ], + "webpack": false, + "deleteOutDir": true, + "watchAssets": false + } +} \ No newline at end of file diff --git a/package.json b/package.json index e67de54..ffa5247 100644 --- a/package.json +++ b/package.json @@ -1,45 +1,135 @@ { - "name": "@plastichub/template", - "description": "", - "version": "0.3.1", - "main": "main.js", - "typings": "index.d.ts", - "publishConfig": { - "access": "public" + "name": "nestjs-boilerplate", + "version": "1.0.0", + "description": "NestJs Boilerplate", + "repository": { + "type": "git" }, - "bin": { - "osr-bin": "main.js" + "author": { + "name": "Fedi DAYEG", + "email": "fedi.dayeg@gmail.com" + }, + "private": true, + "license": "MIT", + "scripts": { + "upgrade:package": "ncu -u", + "prebuild": "rimraf dist", + "build": "nest build", + "format": "yarn format:src && yarn format:test", + "format:src": "prettier --write src/**/*.ts", + "format:test": "prettier --write test/**/*.ts", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/src/main", + "lint": "yarn lint:src && yarn lint:test", + "lint:fix": "eslint --ext .ts,.tsx '{src,test}/**/*.ts' --fix --no-error-on-unmatched-pattern", + "lint:src": "eslint --ext .ts,.tsx 'src/**/*.ts' --no-error-on-unmatched-pattern", + "lint:test": "eslint --ext .ts,.tsx 'test/**/*.ts' --no-error-on-unmatched-pattern", + "test": "yarn test:unit && yarn test:integration && yarn test:e2e", + "test:unit": "jest --config test/unit/jest.json --passWithNoTests --forceExit", + "test:integration": "jest --config test/integration/jest.json --passWithNoTests --forceExit", + "test:e2e": "jest --runInBand --config test/e2e/jest.json --verbose --passWithNoTests --forceExit", + "prepare": "husky install", + "deadcode": "ts-prune --project tsconfig.json --skip *.json", + "deadcode:filter": "ts-prune --project tsconfig.json --skip *.json | grep -v '(used in module)'", + "deadcode:count": "ts-prune --project tsconfig.json --skip *.json | grep -v '(used in module)' | wc -l", + "spell": "yarn spell:src && yarn spell:test", + "spell:src": "cspell lint --config cspell.json src/**/*.ts --color --gitignore --no-must-find-files --no-summary --no-progress || true", + "spell:test": "cspell lint --config cspell.json test/**/*.ts --color --gitignore --no-must-find-files --no-summary --no-progress || true", + "seed:setting": "nestjs-command seed:setting", + "seed:apikey": "nestjs-command seed:apikey", + "seed:permission": "nestjs-command seed:permission", + "seed:role": "nestjs-command seed:role", + "seed:user": "nestjs-command seed:user", + "rollback:setting": "nestjs-command remove:setting", + "rollback:apikey": "nestjs-command remove:apikey", + "rollback:permission": "nestjs-command remove:permission", + "rollback:role": "nestjs-command remove:role", + "rollback:user": "nestjs-command remove:user", + "seed": "yarn seed:setting && yarn seed:permission && yarn seed:role && yarn seed:user && yarn seed:apikey", + "rollback": "yarn rollback:setting && yarn rollback:apikey && yarn rollback:user && yarn rollback:role && yarn rollback:permission" }, "dependencies": { - "@types/node": "^14.17.5", - "@types/yargs": "^17.0.2", - "chalk": "^2.4.1", - "convert-units": "^2.3.4", - "env-var": "^7.0.1", - "typescript": "^4.3.5", - "yargs": "^14.2.3", - "yargs-parser": "^15.0.3" + "@aws-sdk/client-s3": "^3.292.0", + "@faker-js/faker": "^7.6.0", + "@joi/date": "^2.1.0", + "@nestjs/axios": "^2.0.0", + "@nestjs/common": "^9.3.10", + "@nestjs/config": "^2.3.1", + "@nestjs/core": "^9.3.10", + "@nestjs/jwt": "^10.0.2", + "@nestjs/mongoose": "^9.2.1", + "@nestjs/passport": "^9.0.3", + "@nestjs/platform-express": "^9.3.10", + "@nestjs/schedule": "^2.2.0", + "@nestjs/swagger": "^6.2.1", + "@nestjs/terminus": "^9.2.1", + "@nestjs/throttler": "^4.0.0", + "@types/response-time": "^2.3.5", + "bcryptjs": "^2.4.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "crypto-js": "^4.1.1", + "geolib": "^3.3.3", + "helmet": "^6.0.1", + "joi": "^17.8.4", + "moment": "^2.29.4", + "mongoose": "^7.0.2", + "morgan": "^1.10.0", + "nest-winston": "^1.9.1", + "nestjs-command": "^3.1.3", + "nestjs-i18n": "^10.2.6", + "passport": "^0.6.0", + "passport-headerapikey": "^1.2.2", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.1.13", + "response-time": "^2.3.2", + "rimraf": "^4.4.0", + "rotating-file-stream": "^3.1.0", + "rxjs": "^7.8.0", + "ua-parser-js": "^1.0.34", + "winston": "^3.8.2", + "winston-daily-rotate-file": "^4.7.1", + "xlsx": "^0.18.5", + "yargs": "^17.7.1", + "yarn": "^1.22.19" }, - "scripts": { - "test": "tsc; mocha --full-trace mocha \"spec/**/*.spec.js\"", - "test-with-coverage": "istanbul cover node_modules/.bin/_mocha -- 'spec/**/*.spec.js'", - "lint": "tslint --project=./tsconfig.json", - "build": "tsc -p .", - "dev": "tsc -p . --declaration -w", - "typings": "tsc --declaration", - "docs": "npx typedoc src/index.ts", - "dev-test-watch": "mocha-typescript-watch" - }, - "homepage": "https://git.osr-plastic.org/plastichub/lib-content", - "repository": { - "type": "git", - "url": "https://git.osr-plastic.org/plastichub/lib-content.git" - }, - "engines": { - "node": ">= 14.0.0" - }, - "license": "BSD-3-Clause", - "keywords": [ - "typescript" - ] + "devDependencies": { + "@nestjs/cli": "^9.2.0", + "@nestjs/schematics": "^9.0.4", + "@nestjs/testing": "^9.3.10", + "@types/bcryptjs": "^2.4.2", + "@types/bytes": "^3.1.1", + "@types/cors": "^2.8.13", + "@types/cron": "^2.0.0", + "@types/crypto-js": "^4.1.1", + "@types/express": "^4.17.17", + "@types/jest": "^29.4.4", + "@types/lodash": "^4.14.191", + "@types/morgan": "^1.9.4", + "@types/ms": "^0.7.31", + "@types/multer": "^1.4.7", + "@types/node": "^18.15.3", + "@types/passport-jwt": "^3.0.8", + "@types/supertest": "^2.0.12", + "@types/ua-parser-js": "^0.7.36", + "@types/uuid": "^9.0.1", + "@typescript-eslint/eslint-plugin": "^5.55.0", + "@typescript-eslint/parser": "^5.55.0", + "cspell": "^6.30.0", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-import": "^2.27.5", + "husky": "^8.0.3", + "jest": "^29.5.0", + "prettier": "^2.8.4", + "supertest": "^6.3.3", + "ts-jest": "^29.0.5", + "ts-loader": "^9.4.2", + "ts-node": "^10.9.1", + "ts-prune": "^0.10.3", + "tsconfig-paths": "^4.1.2", + "typescript": "^5.0.2" + } } diff --git a/scripts/docker/dockerfile.prod b/scripts/docker/dockerfile.prod new file mode 100644 index 0000000..2865d15 --- /dev/null +++ b/scripts/docker/dockerfile.prod @@ -0,0 +1,31 @@ +# Test Image +FROM node:lts-alpine as builder +LABEL maintainer "fedi.dayeg@gmail.com" + +WORKDIR /app +COPY package.json yarn.lock ./ + +RUN set -x && yarn --production=false + +COPY . . + +RUN yarn build + +# Production Image +FROM node:lts-alpine as main +LABEL maintainer "fedi.dayeg@gmail.com" + +ARG NODE_ENV=production +ENV NODE_ENV=${NODE_ENV} + +WORKDIR /app +EXPOSE 3000 + +COPY package.json yarn.lock ./ +RUN touch .env + +RUN set -x && yarn --production=true + +COPY --from=builder /app/dist ./dist + +CMD ["yarn", "start:prod"] \ No newline at end of file diff --git a/scripts/jenkinsfile b/scripts/jenkinsfile new file mode 100644 index 0000000..9f41c68 --- /dev/null +++ b/scripts/jenkinsfile @@ -0,0 +1,193 @@ +def commit_id + +pipeline { + agent any + options { + skipDefaultCheckout true + } + environment { + BASE_IMAGE='node:lts-alpine' // change this with same version of container image + + APP_NAME = 'nest' // this will be container name + APP_NETWORK = 'app-network' // this network name + APP_PORT = 3000 // this project will serve in port 0.0.0.0:3000 + APP_EXPOSE_PORT = 3000 // this project will expose at port 0.0.0.0:3000 + + NODE_JS = 'lts' // depends with our jenkins setting + + HOST_IP = 'xx.xx.xx.xx' // this server ip for your production server + HOST_CREDENTIAL = '2637b88f-8dc8-4395-bd6b-0c6127720a89' // depends with our credentials jenkins + + DOCKER_CREDENTIAL = 'ef108994-1241-4614-aab5-2aadd7a72284' // depends with our credentials jenkins + DOCKER_FILE= './prod/dockerfile' + DOCKER_USERNAME = 'nest' // docker hub username + DOCKER_REGISTRY = 'https://index.docker.io/v1/' + + GIT = 'Default' // depends with our jenkins setting + GIT_BRANCH = 'main' // git branch + GIT_CREDENTIAL = '86535ad6-5d74-48c0-9852-bddbe1fbaff6' // depends with our credentials jenkins + GIT_URL = 'git@github.com:fedi-dayeg/nestjs-mongoose.git' + } + tools { + nodejs NODE_JS + git GIT + } + stages { + stage('Prepare') { + steps { + cleanWs() + checkout scm + + sh 'node --version && npm --version && yarn --version' + sh 'docker --version' + sh 'docker ps -a' + + script{ + def nodeContainer = docker.image(BASE_IMAGE) + nodeContainer.pull() + nodeContainer.inside { + sh 'node --version' + sh 'npm --version' + sh 'yarn --version' + } + } + } + } + stage('Clone') { + steps { + git branch: GIT_BRANCH, + credentialsId: GIT_CREDENTIAL, + url: GIT_URL + + sh "git rev-parse --short HEAD > .git/commit-id" + sh "grep -o '\"version\": \"[^\"]*' package.json | grep -o '[^\"]*\$' > .git/version-id" + + script{ + commit_id = readFile('.git/commit-id').trim() + } + } + } + stage('Build'){ + steps{ + script{ + def app_image = "${DOCKER_USERNAME}/${APP_NAME}-builder:${commit_id}" + docker.build(app_image, "--target builder -f ${DOCKER_FILE} .") + } + } + } + stage('Unit Test') { + steps { + script{ + def app_image = "${DOCKER_USERNAME}/${APP_NAME}-builder:${commit_id}" + def container = "${APP_NAME}-testing" + + try{ + sh "docker stop ${container} && docker rm ${container}" + }catch(e){} + + try{ + sh "docker network create ${APP_NETWORK} --driver=bridge" + }catch(e){} + + sh "docker run --rm --network ${APP_NETWORK} \ + --volume /app/${APP_NAME}/.env:/app/.env \ + --name ${container} \ + ${app_image} \ + sh -c 'yarn test:unit'" + } + + } + } + stage('Push') { + steps { + script{ + def version_id = readFile('.git/version-id').trim() + def app_image = "${DOCKER_USERNAME}/${APP_NAME}:${commit_id}" + + def app = docker.build(app_image, "--target main -f ${DOCKER_FILE} .") + docker.withRegistry(DOCKER_REGISTRY, DOCKER_CREDENTIAL) { + app.push('latest') + app.push("v${version_id}") + app.push("v${version_id}_sha-${commit_id}") + } + } + + } + } + stage('Deploy') { + steps { + script{ + def version_id = readFile('.git/version-id').trim() + def app_image = "${DOCKER_USERNAME}/${APP_NAME}:v${version_id}_sha-${commit_id}" + + def remote = [:] + remote.name = APP_NAME + remote.host = HOST_IP + remote.allowAnyHosts = true + withCredentials([sshUserPrivateKey(credentialsId: HOST_CREDENTIAL, keyFileVariable: 'IDENTITY', usernameVariable: 'USERNAME')]) { + + remote.user = USERNAME + remote.identityFile = IDENTITY + + try{ + sshCommand remote: remote, command: "docker stop ${APP_NAME} && docker rm ${APP_NAME}" + }catch(e){} + + try{ + sshCommand remote: remote, command: "docker network create ${APP_NETWORK} --driver=bridge" + }catch(e){} + + sshCommand remote: remote, command: "docker run -itd \ + --hostname ${APP_NAME} \ + --publish ${APP_EXPOSE_PORT}:${APP_PORT} \ + --network ${APP_NETWORK} \ + --volume /app/${APP_NAME}/logs/:/app/logs/ \ + --volume /app/${APP_NAME}/.env:/app/.env \ + --restart unless-stopped \ + --name ${APP_NAME} ${app_image}" + } + } + } + } + stage('Clean'){ + steps { + script{ + def remote = [:] + remote.name = APP_NAME + remote.host = HOST_IP + remote.allowAnyHosts = true + withCredentials([sshUserPrivateKey(credentialsId: HOST_CREDENTIAL, keyFileVariable: 'IDENTITY', usernameVariable: 'USERNAME')]) { + + remote.user = USERNAME + remote.identityFile = IDENTITY + + try{ + sshCommand remote: remote, command: "docker container prune --force" + }catch(e){} + + try{ + sshCommand remote: remote, command: "docker image prune --force" + }catch(e){} + + try{ + sshCommand remote: remote, command: "docker rmi \$(docker images **/${APP_NAME}** -q) --force" + }catch(e){} + } + + try{ + sh "docker container prune --force" + }catch(e){} + + try{ + sh "docker image prune --force" + }catch(e){} + + try{ + sh "docker rmi \$(docker images **/${APP_NAME}** -q) --force" + }catch(e){} + + } + } + } + } +} \ No newline at end of file diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/app.module.ts b/src/app/app.module.ts new file mode 100644 index 0000000..451a91a --- /dev/null +++ b/src/app/app.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { JobsModule } from 'src/jobs/jobs.module'; +import { AppController } from './controllers/app.controller'; +import { RouterModule } from 'src/router/router.module'; +import { CommonModule } from 'src/common/common.module'; + +@Module({ + controllers: [AppController], + providers: [], + imports: [ + CommonModule, + + // Jobs + JobsModule.forRoot(), + + // Routes + RouterModule.forRoot(), + ], +}) +export class AppModule {} diff --git a/src/app/constants/app.constant.ts b/src/app/constants/app.constant.ts new file mode 100644 index 0000000..58273f5 --- /dev/null +++ b/src/app/constants/app.constant.ts @@ -0,0 +1 @@ +export const APP_LANGUAGE = 'en'; diff --git a/src/app/constants/app.enum.constant.ts b/src/app/constants/app.enum.constant.ts new file mode 100644 index 0000000..96033ea --- /dev/null +++ b/src/app/constants/app.enum.constant.ts @@ -0,0 +1,5 @@ +export enum ENUM_APP_ENVIRONMENT { + PRODUCTION = 'production', + STAGING = 'staging', + DEVELOPMENT = 'development', +} diff --git a/src/app/controllers/app.controller.ts b/src/app/controllers/app.controller.ts new file mode 100644 index 0000000..12a1224 --- /dev/null +++ b/src/app/controllers/app.controller.ts @@ -0,0 +1,81 @@ +import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ApiTags } from '@nestjs/swagger'; +import { AppHelloApiKeyDoc, AppHelloDoc } from 'src/app/docs/app.doc'; +import { AppHelloSerialization } from 'src/app/serializations/app.hello.serialization'; +import { ApiKeyProtected } from 'src/common/api-key/decorators/api-key.decorator'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ENUM_LOGGER_ACTION } from 'src/common/logger/constants/logger.enum.constant'; +import { Logger } from 'src/common/logger/decorators/logger.decorator'; +import { RequestUserAgent } from 'src/common/request/decorators/request.decorator'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; +import { IResult } from 'ua-parser-js'; + +@ApiTags('hello') +@Controller({ + version: VERSION_NEUTRAL, + path: '/', +}) +export class AppController { + private readonly serviceName: string; + + constructor( + private readonly configService: ConfigService, + private readonly helperDateService: HelperDateService + ) { + this.serviceName = this.configService.get('app.name'); + } + + @AppHelloDoc() + @Response('app.hello', { serialization: AppHelloSerialization }) + @Logger(ENUM_LOGGER_ACTION.TEST, { tags: ['test'] }) + @Get('/hello') + async hello(@RequestUserAgent() userAgent: IResult): Promise { + const newDate = this.helperDateService.create(); + + return { + _metadata: { + customProperty: { + messageProperties: { + serviceName: this.serviceName + }, + + }, + }, + data: { + userAgent, + date: newDate, + format: this.helperDateService.format(newDate), + timestamp: this.helperDateService.timestamp(newDate), + }, + }; + } + + @AppHelloApiKeyDoc() + @Response('app.hello', { serialization: AppHelloSerialization }) + @Logger(ENUM_LOGGER_ACTION.TEST, { tags: ['test'] }) + @ApiKeyProtected() + @Get('/hello/api-key') + async helloApiKey( + @RequestUserAgent() userAgent: IResult + ): Promise { + const newDate = this.helperDateService.create(); + + return { + _metadata: { + customProperty: { + messageProperties: { + serviceName: this.serviceName + } + }, + }, + data: { + userAgent, + date: newDate, + format: this.helperDateService.format(newDate), + timestamp: this.helperDateService.timestamp(newDate), + }, + }; + } +} diff --git a/src/app/docs/app.doc.ts b/src/app/docs/app.doc.ts new file mode 100644 index 0000000..54730f9 --- /dev/null +++ b/src/app/docs/app.doc.ts @@ -0,0 +1,30 @@ +import { applyDecorators } from '@nestjs/common'; +import { AppHelloSerialization } from 'src/app/serializations/app.hello.serialization'; +import { Doc } from 'src/common/doc/decorators/doc.decorator'; + +export function AppHelloDoc(): MethodDecorator { + return applyDecorators( + Doc('app.hello', { + response: { + serialization: AppHelloSerialization, + }, + }) + ); +} + +export function AppHelloApiKeyDoc(): MethodDecorator { + return applyDecorators( + Doc('app.helloApiKey', { + auth: { + apiKey: true, + }, + requestHeader: { + timestamp: true, + userAgent: true, + }, + response: { + serialization: AppHelloSerialization, + }, + }) + ); +} diff --git a/src/app/serializations/app.hello.serialization.ts b/src/app/serializations/app.hello.serialization.ts new file mode 100644 index 0000000..6e5a75f --- /dev/null +++ b/src/app/serializations/app.hello.serialization.ts @@ -0,0 +1,30 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IResult } from 'ua-parser-js'; + +export class AppHelloSerialization { + @ApiProperty({ + example: { + ua: 'PostmanRuntime/7.29.0', + browser: {}, + engine: {}, + os: {}, + device: {}, + cpu: {}, + }, + }) + readonly userAgent: IResult; + + @ApiProperty({ example: faker.date.recent() }) + @Type(() => String) + readonly date: Date; + + @ApiProperty({ example: faker.date.recent() }) + readonly format: string; + + @ApiProperty({ + example: 1660190937231, + }) + readonly timestamp: number; +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..e6dcf6f --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,22 @@ +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { CommandModule, CommandService } from 'nestjs-command'; +import { MigrationModule } from './migration/migration.module'; + +async function bootstrap() { + const app = await NestFactory.createApplicationContext(MigrationModule, { + logger: ['error'], + }); + + const logger = new Logger(); + + try { + await app.select(CommandModule).get(CommandService).exec(); + process.exit(0); + } catch (err: unknown) { + logger.error(err, 'Migration'); + process.exit(1); + } +} + +bootstrap(); diff --git a/src/common/api-key/api-key.module.ts b/src/common/api-key/api-key.module.ts new file mode 100644 index 0000000..2c22ced --- /dev/null +++ b/src/common/api-key/api-key.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ApiKeyXApiKeyStrategy } from 'src/common/api-key/guards/x-api-key/api-key.x-api-key.strategy'; +import { ApiKeyRepositoryModule } from 'src/common/api-key/repository/api-key.repository.module'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; + +@Module({ + providers: [ApiKeyService, ApiKeyXApiKeyStrategy], + exports: [ApiKeyService], + controllers: [], + imports: [ApiKeyRepositoryModule], +}) +export class ApiKeyModule {} diff --git a/src/common/api-key/constants/api-key.constant.ts b/src/common/api-key/constants/api-key.constant.ts new file mode 100644 index 0000000..5efb20c --- /dev/null +++ b/src/common/api-key/constants/api-key.constant.ts @@ -0,0 +1 @@ +export const API_KEY_ACTIVE_META_KEY = 'ApiKeyActiveMetaKey'; diff --git a/src/common/api-key/constants/api-key.doc.ts b/src/common/api-key/constants/api-key.doc.ts new file mode 100644 index 0000000..947dc0a --- /dev/null +++ b/src/common/api-key/constants/api-key.doc.ts @@ -0,0 +1,22 @@ +import { faker } from '@faker-js/faker'; + +export const ApiKeyDocQueryIsActive = [ + { + name: 'isActive', + allowEmptyValue: false, + required: true, + type: 'string', + example: 'true,false', + description: "boolean value with ',' delimiter", + }, +]; + +export const ApiKeyDocParamsGet = [ + { + name: 'apiKey', + allowEmptyValue: false, + required: true, + type: 'string', + example: faker.datatype.uuid(), + }, +]; diff --git a/src/common/api-key/constants/api-key.list.constant.ts b/src/common/api-key/constants/api-key.list.constant.ts new file mode 100644 index 0000000..3a71adf --- /dev/null +++ b/src/common/api-key/constants/api-key.list.constant.ts @@ -0,0 +1,9 @@ +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; + +export const API_KEY_DEFAULT_PER_PAGE = 20; +export const API_KEY_DEFAULT_ORDER_BY = 'createdAt'; +export const API_KEY_DEFAULT_ORDER_DIRECTION = + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC; +export const API_KEY_DEFAULT_AVAILABLE_ORDER_BY = ['name', 'key', 'createdAt']; +export const API_KEY_DEFAULT_AVAILABLE_SEARCH = ['name', 'key']; +export const API_KEY_DEFAULT_IS_ACTIVE = [true, false]; diff --git a/src/common/api-key/constants/api-key.status-code.constant.ts b/src/common/api-key/constants/api-key.status-code.constant.ts new file mode 100644 index 0000000..577ea22 --- /dev/null +++ b/src/common/api-key/constants/api-key.status-code.constant.ts @@ -0,0 +1,10 @@ +export enum ENUM_API_KEY_STATUS_CODE_ERROR { + API_KEY_NEEDED_ERROR = 5120, + API_KEY_NOT_FOUND_ERROR = 5121, + API_KEY_IS_ACTIVE_ERROR = 5122, + API_KEY_EXPIRED_ERROR = 5123, + API_KEY_INACTIVE_ERROR = 5124, + API_KEY_INVALID_ERROR = 5125, + API_KEY_WRONG_ERROR = 5126, + API_KEY_EXIST_ERROR = 5127, +} diff --git a/src/common/api-key/controllers/api-key.admin.controller.ts b/src/common/api-key/controllers/api-key.admin.controller.ts new file mode 100644 index 0000000..96a0116 --- /dev/null +++ b/src/common/api-key/controllers/api-key.admin.controller.ts @@ -0,0 +1,118 @@ +import { + Body, + Controller, + InternalServerErrorException, + Patch, + Put, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { + ApiKeyUpdateActiveGuard, + ApiKeyUpdateGuard, + ApiKeyUpdateInactiveGuard, +} from 'src/common/api-key/decorators/api-key.admin.decorator'; +import { GetApiKey } from 'src/common/api-key/decorators/api-key.decorator'; +import { + ApiKeyActiveDoc, + ApiKeyInactiveDoc, + ApiKeyUpdateDoc, +} from 'src/common/api-key/docs/api-key.admin.doc'; +import { ApiKeyRequestDto } from 'src/common/api-key/dtos/api-key.request.dto'; +import { ApiKeyUpdateDateDto } from 'src/common/api-key/dtos/api-key.update-date.dto'; +import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { AuthJwtAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator'; +import { AuthPermissionProtected } from 'src/common/auth/decorators/auth.permission.decorator'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { RequestParamGuard } from 'src/common/request/decorators/request.decorator'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; + +@ApiTags('admin.apiKey') +@Controller({ + version: '1', + path: '/api-key', +}) +export class ApiKeyAdminController { + constructor(private readonly apiKeyService: ApiKeyService) {} + + @ApiKeyInactiveDoc() + @Response('apiKey.inactive') + @ApiKeyUpdateInactiveGuard() + @RequestParamGuard(ApiKeyRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.API_KEY_READ, + ENUM_AUTH_PERMISSIONS.API_KEY_UPDATE, + ENUM_AUTH_PERMISSIONS.API_KEY_INACTIVE + ) + @AuthJwtAccessProtected() + @Patch('/update/:apiKey/inactive') + async inactive(@GetApiKey() apiKey: ApiKeyDoc): Promise { + try { + await this.apiKeyService.inactive(apiKey); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @ApiKeyActiveDoc() + @Response('apiKey.active') + @ApiKeyUpdateActiveGuard() + @RequestParamGuard(ApiKeyRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.API_KEY_READ, + ENUM_AUTH_PERMISSIONS.API_KEY_UPDATE, + ENUM_AUTH_PERMISSIONS.API_KEY_ACTIVE + ) + @AuthJwtAccessProtected() + @Patch('/update/:apiKey/active') + async active(@GetApiKey() apiKey: ApiKeyDoc): Promise { + try { + await this.apiKeyService.active(apiKey); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @ApiKeyUpdateDoc() + @Response('apiKey.updateDate', { serialization: ResponseIdSerialization }) + @ApiKeyUpdateGuard() + @RequestParamGuard(ApiKeyRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.API_KEY_READ, + ENUM_AUTH_PERMISSIONS.API_KEY_UPDATE, + ENUM_AUTH_PERMISSIONS.API_KEY_UPDATE_DATE + ) + @AuthJwtAccessProtected() + @Put('/update/:apiKey/date') + async updateDate( + @Body() body: ApiKeyUpdateDateDto, + @GetApiKey() apiKey: ApiKeyDoc + ): Promise { + try { + await this.apiKeyService.updateDate(apiKey, body); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return { data: { _id: apiKey._id } }; + } +} diff --git a/src/common/api-key/controllers/api-key.controller.ts b/src/common/api-key/controllers/api-key.controller.ts new file mode 100644 index 0000000..810c938 --- /dev/null +++ b/src/common/api-key/controllers/api-key.controller.ts @@ -0,0 +1,222 @@ +import { + Body, + ConflictException, + Controller, + Get, + InternalServerErrorException, + Patch, + Post, + Put, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { + API_KEY_DEFAULT_AVAILABLE_ORDER_BY, + API_KEY_DEFAULT_AVAILABLE_SEARCH, + API_KEY_DEFAULT_IS_ACTIVE, + API_KEY_DEFAULT_ORDER_BY, + API_KEY_DEFAULT_ORDER_DIRECTION, + API_KEY_DEFAULT_PER_PAGE, +} from 'src/common/api-key/constants/api-key.list.constant'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; +import { + ApiKeyGetGuard, + ApiKeyUpdateGuard, + ApiKeyUpdateResetGuard, +} from 'src/common/api-key/decorators/api-key.admin.decorator'; +import { GetApiKey } from 'src/common/api-key/decorators/api-key.decorator'; +import { + ApiKeyCreateDoc, + ApiKeyGetDoc, + ApiKeyListDoc, + ApiKeyResetDoc, + ApiKeyUpdateDoc, +} from 'src/common/api-key/docs/api-key.admin.doc'; +import { ApiKeyCreateDto } from 'src/common/api-key/dtos/api-key.create.dto'; +import { ApiKeyRequestDto } from 'src/common/api-key/dtos/api-key.request.dto'; +import { ApiKeyUpdateDto } from 'src/common/api-key/dtos/api-key.update.dto'; +import { IApiKeyCreated } from 'src/common/api-key/interfaces/api-key.interface'; +import { + ApiKeyDoc, + ApiKeyEntity, +} from 'src/common/api-key/repository/entities/api-key.entity'; +import { ApiKeyCreateSerialization } from 'src/common/api-key/serializations/api-key.create.serialization'; +import { ApiKeyGetSerialization } from 'src/common/api-key/serializations/api-key.get.serialization'; +import { ApiKeyListSerialization } from 'src/common/api-key/serializations/api-key.list.serialization'; +import { ApiKeyResetSerialization } from 'src/common/api-key/serializations/api-key.reset.serialization'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { + AuthJwtAccessProtected, + AuthJwtPayload, +} from 'src/common/auth/decorators/auth.jwt.decorator'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { + PaginationQuery, + PaginationQueryFilterInBoolean, +} from 'src/common/pagination/decorators/pagination.decorator'; +import { PaginationListDto } from 'src/common/pagination/dtos/pagination.list.dto'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { RequestParamGuard } from 'src/common/request/decorators/request.decorator'; +import { + Response, + ResponsePaging, +} from 'src/common/response/decorators/response.decorator'; +import { + IResponse, + IResponsePaging, +} from 'src/common/response/interfaces/response.interface'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; + +@ApiTags('apiKey') +@Controller({ + version: '1', + path: '/api-key', +}) +export class ApiKeyController { + constructor( + private readonly apiKeyService: ApiKeyService, + private readonly paginationService: PaginationService + ) {} + + @ApiKeyListDoc() + @ResponsePaging('apiKey.list', { + serialization: ApiKeyListSerialization, + }) + @AuthJwtAccessProtected() + @Get('/list') + async list( + @PaginationQuery( + API_KEY_DEFAULT_PER_PAGE, + API_KEY_DEFAULT_ORDER_BY, + API_KEY_DEFAULT_ORDER_DIRECTION, + API_KEY_DEFAULT_AVAILABLE_SEARCH, + API_KEY_DEFAULT_AVAILABLE_ORDER_BY + ) + { _search, _limit, _offset, _order }: PaginationListDto, + @PaginationQueryFilterInBoolean('isActive', API_KEY_DEFAULT_IS_ACTIVE) + isActive: Record + ): Promise { + const find: Record = { + ..._search, + ...isActive, + }; + + const apiKeys: ApiKeyEntity[] = await this.apiKeyService.findAll(find, { + paging: { + limit: _limit, + offset: _offset, + }, + order: _order, + }); + const total: number = await this.apiKeyService.getTotal(find); + const totalPage: number = this.paginationService.totalPage( + total, + _limit + ); + + return { + _pagination: { totalPage, total }, + data: apiKeys, + }; + } + + @ApiKeyGetDoc() + @Response('apiKey.get', { + serialization: ApiKeyGetSerialization, + }) + @ApiKeyGetGuard() + @RequestParamGuard(ApiKeyRequestDto) + @AuthJwtAccessProtected() + @Get('get/:apiKey') + async get(@GetApiKey(true) apiKey: ApiKeyEntity): Promise { + return { data: apiKey }; + } + + @ApiKeyCreateDoc() + @Response('apiKey.create', { serialization: ApiKeyCreateSerialization }) + @AuthJwtAccessProtected() + @Post('/create') + async create( + @AuthJwtPayload('_id') _id: string, + @Body() body: ApiKeyCreateDto + ): Promise { + const checkUser: boolean = await this.apiKeyService.existByUser(_id); + if (checkUser) { + throw new ConflictException({ + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXIST_ERROR, + message: 'apiKey.error.exist', + }); + } + + try { + const created: IApiKeyCreated = await this.apiKeyService.create( + _id, + body + ); + + return { + data: { + _id: created.doc._id, + secret: created.secret, + }, + }; + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + } + + @ApiKeyResetDoc() + @Response('apiKey.reset', { serialization: ApiKeyResetSerialization }) + @ApiKeyUpdateResetGuard() + @RequestParamGuard(ApiKeyRequestDto) + @AuthJwtAccessProtected() + @Patch('/update/:apiKey/reset') + async reset(@GetApiKey() apiKey: ApiKeyDoc): Promise { + try { + const secret: string = await this.apiKeyService.createSecret(); + const updated: ApiKeyDoc = await this.apiKeyService.reset( + apiKey, + secret + ); + + return { + data: { + _id: updated._id, + secret, + }, + }; + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + } + + @ApiKeyUpdateDoc() + @Response('apiKey.update', { serialization: ResponseIdSerialization }) + @ApiKeyUpdateGuard() + @RequestParamGuard(ApiKeyRequestDto) + @AuthJwtAccessProtected() + @Put('/update/:apiKey') + async updateName( + @Body() body: ApiKeyUpdateDto, + @GetApiKey() apiKey: ApiKeyDoc + ): Promise { + try { + await this.apiKeyService.update(apiKey, body); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return { data: { _id: apiKey._id } }; + } +} \ No newline at end of file diff --git a/src/common/api-key/decorators/api-key.admin.decorator.ts b/src/common/api-key/decorators/api-key.admin.decorator.ts new file mode 100644 index 0000000..30ae7c8 --- /dev/null +++ b/src/common/api-key/decorators/api-key.admin.decorator.ts @@ -0,0 +1,60 @@ +import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'; +import { API_KEY_ACTIVE_META_KEY } from 'src/common/api-key/constants/api-key.constant'; +import { ApiKeyActiveGuard } from 'src/common/api-key/guards/api-key.active.guard'; +import { ApiKeyExpiredGuard } from 'src/common/api-key/guards/api-key.expired.guard'; +import { ApiKeyNotFoundGuard } from 'src/common/api-key/guards/api-key.not-found.guard'; +import { ApiKeyPutToRequestGuard } from 'src/common/api-key/guards/api-key.put-to-request.guard'; + +export function ApiKeyGetGuard(): MethodDecorator { + return applyDecorators( + UseGuards(ApiKeyPutToRequestGuard, ApiKeyNotFoundGuard) + ); +} + +export function ApiKeyUpdateGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + ApiKeyPutToRequestGuard, + ApiKeyNotFoundGuard, + ApiKeyActiveGuard, + ApiKeyExpiredGuard + ), + SetMetadata(API_KEY_ACTIVE_META_KEY, [true]) + ); +} + +export function ApiKeyUpdateResetGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + ApiKeyPutToRequestGuard, + ApiKeyNotFoundGuard, + ApiKeyActiveGuard, + ApiKeyExpiredGuard + ), + SetMetadata(API_KEY_ACTIVE_META_KEY, [true]) + ); +} + +export function ApiKeyUpdateActiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + ApiKeyPutToRequestGuard, + ApiKeyNotFoundGuard, + ApiKeyActiveGuard, + ApiKeyExpiredGuard + ), + SetMetadata(API_KEY_ACTIVE_META_KEY, [false]) + ); +} + +export function ApiKeyUpdateInactiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + ApiKeyPutToRequestGuard, + ApiKeyNotFoundGuard, + ApiKeyActiveGuard, + ApiKeyExpiredGuard + ), + SetMetadata(API_KEY_ACTIVE_META_KEY, [true]) + ); +} diff --git a/src/common/api-key/decorators/api-key.decorator.ts b/src/common/api-key/decorators/api-key.decorator.ts new file mode 100644 index 0000000..31aa783 --- /dev/null +++ b/src/common/api-key/decorators/api-key.decorator.ts @@ -0,0 +1,27 @@ +import { + applyDecorators, + createParamDecorator, + ExecutionContext, + UseGuards, +} from '@nestjs/common'; +import { ApiKeyXApiKeyGuard } from 'src/common/api-key/guards/x-api-key/api-key.x-api-key.guard'; +import { IApiKeyPayload } from 'src/common/api-key/interfaces/api-key.interface'; +import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity'; + +export const ApiKeyPayload: () => ParameterDecorator = createParamDecorator( + (data: string, ctx: ExecutionContext): IApiKeyPayload => { + const { apiKey } = ctx.switchToHttp().getRequest(); + return data ? apiKey[data] : apiKey; + } +); + +export function ApiKeyProtected(): MethodDecorator { + return applyDecorators(UseGuards(ApiKeyXApiKeyGuard)); +} + +export const GetApiKey = createParamDecorator( + (returnPlain: boolean, ctx: ExecutionContext): ApiKeyDoc => { + const { __apiKey } = ctx.switchToHttp().getRequest(); + return returnPlain ? __apiKey.toObject() : __apiKey; + } +); diff --git a/src/common/api-key/docs/api-key.admin.doc.ts b/src/common/api-key/docs/api-key.admin.doc.ts new file mode 100644 index 0000000..c2a2ccc --- /dev/null +++ b/src/common/api-key/docs/api-key.admin.doc.ts @@ -0,0 +1,119 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { + ApiKeyDocParamsGet, + ApiKeyDocQueryIsActive, +} from 'src/common/api-key/constants/api-key.doc'; +import { ApiKeyCreateSerialization } from 'src/common/api-key/serializations/api-key.create.serialization'; +import { ApiKeyGetSerialization } from 'src/common/api-key/serializations/api-key.get.serialization'; +import { ApiKeyListSerialization } from 'src/common/api-key/serializations/api-key.list.serialization'; +import { Doc, DocPaging } from 'src/common/doc/decorators/doc.decorator'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; + +export function ApiKeyListDoc(): MethodDecorator { + return applyDecorators( + DocPaging('apiKey.list', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + queries: ApiKeyDocQueryIsActive, + }, + response: { + serialization: ApiKeyListSerialization, + }, + }) + ); +} + +export function ApiKeyGetDoc(): MethodDecorator { + return applyDecorators( + Doc('apiKey.get', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: ApiKeyDocParamsGet, + }, + response: { serialization: ApiKeyGetSerialization }, + }) + ); +} + +export function ApiKeyCreateDoc(): MethodDecorator { + return applyDecorators( + Doc('apiKey.create', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + response: { + httpStatus: HttpStatus.CREATED, + serialization: ApiKeyCreateSerialization, + }, + }) + ); +} + +export function ApiKeyActiveDoc(): MethodDecorator { + return applyDecorators( + Doc('apiKey.active', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: ApiKeyDocParamsGet, + }, + }) + ); +} + +export function ApiKeyInactiveDoc(): MethodDecorator { + return applyDecorators( + Doc('apiKey.inactive', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: ApiKeyDocParamsGet, + }, + }) + ); +} + +export function ApiKeyResetDoc(): MethodDecorator { + return applyDecorators( + Doc('apiKey.reset', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: ApiKeyDocParamsGet, + }, + response: { + serialization: ApiKeyCreateSerialization, + }, + }) + ); +} + +export function ApiKeyUpdateDoc(): MethodDecorator { + return applyDecorators( + Doc('apiKey.update', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: ApiKeyDocParamsGet, + }, + response: { + serialization: ResponseIdSerialization, + }, + }) + ); +} diff --git a/src/common/api-key/dtos/api-key.active.dto.ts b/src/common/api-key/dtos/api-key.active.dto.ts new file mode 100644 index 0000000..c21d079 --- /dev/null +++ b/src/common/api-key/dtos/api-key.active.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsBoolean } from 'class-validator'; + +export class ApiKeyActiveDto { + @ApiProperty({ + name: 'isActive', + required: true, + nullable: false, + }) + @IsBoolean() + @IsNotEmpty() + @IsBoolean() + @IsNotEmpty() + isActive: boolean; +} diff --git a/src/common/api-key/dtos/api-key.create.dto.ts b/src/common/api-key/dtos/api-key.create.dto.ts new file mode 100644 index 0000000..d8c403f --- /dev/null +++ b/src/common/api-key/dtos/api-key.create.dto.ts @@ -0,0 +1,57 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty, PartialType } from '@nestjs/swagger'; +import { + IsNotEmpty, + IsOptional, + IsString, + MaxLength, + MinLength, +} from 'class-validator'; +import { ApiKeyUpdateDateDto } from 'src/common/api-key/dtos/api-key.update-date.dto'; + +export class ApiKeyCreateDto extends PartialType(ApiKeyUpdateDateDto) { + @ApiProperty({ + description: 'Api Key name', + example: `testapiname`, + required: true, + }) + @IsNotEmpty() + @IsString() + @MaxLength(50) + name: string; + + @ApiProperty({ + description: 'Description of api key', + example: 'blabla description', + required: false, + }) + @IsOptional() + @IsString() + @MinLength(1) + @MaxLength(100) + description?: string; +} + +export class ApiKeyCreateRawDto extends ApiKeyCreateDto { + @ApiProperty({ + name: 'key', + example: faker.random.alphaNumeric(10), + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsString() + @MaxLength(50) + key: string; + + @ApiProperty({ + name: 'secret', + example: faker.random.alphaNumeric(20), + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsString() + @MaxLength(100) + secret: string; +} diff --git a/src/common/api-key/dtos/api-key.request.dto.ts b/src/common/api-key/dtos/api-key.request.dto.ts new file mode 100644 index 0000000..a7f3c3f --- /dev/null +++ b/src/common/api-key/dtos/api-key.request.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class ApiKeyRequestDto { + @ApiProperty({ + name: 'apiKey', + description: 'apiKey id', + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsUUID('4') + @Type(() => String) + apiKey: string; +} diff --git a/src/common/api-key/dtos/api-key.update-date.dto.ts b/src/common/api-key/dtos/api-key.update-date.dto.ts new file mode 100644 index 0000000..10f36fb --- /dev/null +++ b/src/common/api-key/dtos/api-key.update-date.dto.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { faker } from '@faker-js/faker'; +import { IsDate, IsNotEmpty } from 'class-validator'; +import { Type } from 'class-transformer'; +import { MinGreaterThanEqual } from 'src/common/request/validations/request.min-greater-than-equal.validation'; +import { MinDateToday } from 'src/common/request/validations/request.min-date-today.validation'; + +export class ApiKeyUpdateDateDto { + @ApiProperty({ + description: 'Api Key start date', + example: faker.date.recent(), + required: false, + nullable: true, + }) + @IsNotEmpty() + @Type(() => Date) + @IsDate() + @MinDateToday() + startDate: Date; + + @ApiProperty({ + description: 'Api Key end date', + example: faker.date.recent(), + required: false, + nullable: true, + }) + @IsNotEmpty() + @Type(() => Date) + @IsDate() + @MinGreaterThanEqual('startDate') + endDate: Date; +} diff --git a/src/common/api-key/dtos/api-key.update.dto.ts b/src/common/api-key/dtos/api-key.update.dto.ts new file mode 100644 index 0000000..5b18839 --- /dev/null +++ b/src/common/api-key/dtos/api-key.update.dto.ts @@ -0,0 +1,7 @@ +import { PickType } from '@nestjs/swagger'; +import { ApiKeyCreateDto } from './api-key.create.dto'; + +export class ApiKeyUpdateDto extends PickType(ApiKeyCreateDto, [ + 'name', + 'description', +] as const) {} diff --git a/src/common/api-key/guards/api-key.active.guard.ts b/src/common/api-key/guards/api-key.active.guard.ts new file mode 100644 index 0000000..c19b0dd --- /dev/null +++ b/src/common/api-key/guards/api-key.active.guard.ts @@ -0,0 +1,36 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { API_KEY_ACTIVE_META_KEY } from 'src/common/api-key/constants/api-key.constant'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; + +@Injectable() +export class ApiKeyActiveGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const required: boolean[] = this.reflector.getAllAndOverride( + API_KEY_ACTIVE_META_KEY, + [context.getHandler(), context.getClass()] + ); + + if (!required) { + return true; + } + + const { __apiKey } = context.switchToHttp().getRequest(); + + if (!required.includes(__apiKey.isActive)) { + throw new BadRequestException({ + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_IS_ACTIVE_ERROR, + message: 'apiKey.error.isActiveInvalid', + }); + } + return true; + } +} diff --git a/src/common/api-key/guards/api-key.expired.guard.ts b/src/common/api-key/guards/api-key.expired.guard.ts new file mode 100644 index 0000000..b512209 --- /dev/null +++ b/src/common/api-key/guards/api-key.expired.guard.ts @@ -0,0 +1,31 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; + +@Injectable() +export class ApiKeyExpiredGuard implements CanActivate { + constructor(private readonly helperDateService: HelperDateService) {} + + async canActivate(context: ExecutionContext): Promise { + const { __apiKey } = context.switchToHttp().getRequest(); + const today: Date = this.helperDateService.create(); + + if ( + __apiKey.startDate && + __apiKey.endDate && + today > __apiKey.endDate + ) { + throw new BadRequestException({ + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR, + message: 'apiKey.error.expired', + }); + } + return true; + } +} diff --git a/src/common/api-key/guards/api-key.not-found.guard.ts b/src/common/api-key/guards/api-key.not-found.guard.ts new file mode 100644 index 0000000..c11bcf7 --- /dev/null +++ b/src/common/api-key/guards/api-key.not-found.guard.ts @@ -0,0 +1,23 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + NotFoundException, +} from '@nestjs/common'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; + +@Injectable() +export class ApiKeyNotFoundGuard implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + const { __apiKey } = context.switchToHttp().getRequest(); + + if (!__apiKey) { + throw new NotFoundException({ + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR, + message: 'apiKey.error.notFound', + }); + } + return true; + } +} diff --git a/src/common/api-key/guards/api-key.put-to-request.guard.ts b/src/common/api-key/guards/api-key.put-to-request.guard.ts new file mode 100644 index 0000000..9db837c --- /dev/null +++ b/src/common/api-key/guards/api-key.put-to-request.guard.ts @@ -0,0 +1,19 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; + +@Injectable() +export class ApiKeyPutToRequestGuard implements CanActivate { + constructor(private readonly apiKeyService: ApiKeyService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { params } = request; + const { apiKey } = params; + + const check: ApiKeyDoc = await this.apiKeyService.findOneById(apiKey); + request.__apiKey = check; + + return true; + } +} diff --git a/src/common/api-key/guards/x-api-key/api-key.x-api-key.guard.ts b/src/common/api-key/guards/x-api-key/api-key.x-api-key.guard.ts new file mode 100644 index 0000000..86dfcd7 --- /dev/null +++ b/src/common/api-key/guards/x-api-key/api-key.x-api-key.guard.ts @@ -0,0 +1,86 @@ +import { AuthGuard } from '@nestjs/passport'; +import { + ExecutionContext, + ForbiddenException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; +import { BadRequestError } from 'passport-headerapikey'; + +@Injectable() +export class ApiKeyXApiKeyGuard extends AuthGuard('api-key') { + constructor(private readonly helperNumberService: HelperNumberService) { + super(); + } + + canActivate(context: ExecutionContext) { + return super.canActivate(context); + } + + handleRequest( + err: Record, + apiKey: IApiKeyPayload, + info: Error | string + ): IApiKeyPayload { + if (err || !apiKey) { + if ( + info instanceof BadRequestError && + info.message === 'Missing API Key' + ) { + throw new UnauthorizedException({ + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NEEDED_ERROR, + message: 'apiKey.error.keyNeeded', + }); + } else if (err) { + const statusCode: number = this.helperNumberService.create( + err.message as string + ); + + if ( + statusCode === + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR + ) { + throw new ForbiddenException({ + statusCode, + message: 'apiKey.error.notFound', + }); + } else if ( + statusCode === + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_IS_ACTIVE_ERROR + ) { + throw new ForbiddenException({ + statusCode, + message: 'apiKey.error.inactive', + }); + } else if ( + statusCode === + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR + ) { + throw new ForbiddenException({ + statusCode, + message: 'apiKey.error.expired', + }); + } else if ( + statusCode === + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_WRONG_ERROR + ) { + throw new ForbiddenException({ + statusCode, + message: 'apiKey.error.wrong', + }); + } + } + + throw new UnauthorizedException({ + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INVALID_ERROR, + message: 'apiKey.error.invalid', + }); + } + + return apiKey; + } +} diff --git a/src/common/api-key/guards/x-api-key/api-key.x-api-key.strategy.ts b/src/common/api-key/guards/x-api-key/api-key.x-api-key.strategy.ts new file mode 100644 index 0000000..dc56be5 --- /dev/null +++ b/src/common/api-key/guards/x-api-key/api-key.x-api-key.strategy.ts @@ -0,0 +1,119 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import Strategy from 'passport-headerapikey'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; +import { ApiKeyEntity } from 'src/common/api-key/repository/entities/api-key.entity'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable() +export class ApiKeyXApiKeyStrategy extends PassportStrategy( + Strategy, + 'api-key' +) { + constructor( + private readonly apiKeyService: ApiKeyService, + private readonly helperDateService: HelperDateService + ) { + super( + { header: 'X-API-KEY', prefix: '' }, + true, + async ( + apiKey: string, + verified: ( + error: Error, + user?: Record, + info?: string | number + ) => Promise, + req: IRequestApp + ) => this.validate(apiKey, verified, req) + ); + } + + async validate( + apiKey: string, + verified: ( + error: Error, + user?: ApiKeyEntity, + info?: string | number + ) => Promise, + req: IRequestApp + ): Promise { + const xApiKey: string[] = apiKey.split(':'); + if (xApiKey.length !== 2) { + verified( + new Error( + `${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INVALID_ERROR}` + ), + null, + null + ); + + return; + } + + const key = xApiKey[0]; + const secret = xApiKey[1]; + const today = this.helperDateService.create(); + const authApi: ApiKeyEntity = + await this.apiKeyService.findOneByActiveKey(key); + + if (!authApi) { + verified( + new Error( + `${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR}` + ), + null, + null + ); + + return; + } else if (!authApi.isActive) { + verified( + new Error( + `${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_IS_ACTIVE_ERROR}` + ), + null, + null + ); + + return; + } else if ( + authApi.startDate && + authApi.endDate && + (authApi.startDate < today || authApi.endDate > today) + ) { + verified( + new Error( + `${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR}` + ), + null, + null + ); + } + + const validateApiKey: boolean = + await this.apiKeyService.validateHashApiKey(secret, authApi.hash); + if (!validateApiKey) { + verified( + new Error( + `${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INVALID_ERROR}` + ), + null, + null + ); + + return; + } + + req.apiKey = { + _id: `${authApi._id}`, + key: authApi.key, + name: authApi.name, + }; + verified(null, authApi); + + return; + } +} diff --git a/src/common/api-key/interfaces/api-key.interface.ts b/src/common/api-key/interfaces/api-key.interface.ts new file mode 100644 index 0000000..c35117b --- /dev/null +++ b/src/common/api-key/interfaces/api-key.interface.ts @@ -0,0 +1,12 @@ +import { ApiKeyDoc } from "src/common/api-key/repository/entities/api-key.entity"; + +export interface IApiKeyPayload { + _id: string; + key: string; + name: string; +} + +export interface IApiKeyCreated { + secret: string; + doc: ApiKeyDoc; +} diff --git a/src/common/api-key/interfaces/api-key.service.interface.ts b/src/common/api-key/interfaces/api-key.service.interface.ts new file mode 100644 index 0000000..dea228d --- /dev/null +++ b/src/common/api-key/interfaces/api-key.service.interface.ts @@ -0,0 +1,92 @@ +import { + ApiKeyCreateDto, + ApiKeyCreateRawDto, +} from 'src/common/api-key/dtos/api-key.create.dto'; +import { ApiKeyUpdateDateDto } from 'src/common/api-key/dtos/api-key.update-date.dto'; +import { ApiKeyUpdateDto } from 'src/common/api-key/dtos/api-key.update.dto'; +import { IApiKeyCreated } from 'src/common/api-key/interfaces/api-key.interface'; +import { + ApiKeyDoc, + ApiKeyEntity, +} from 'src/common/api-key/repository/entities/api-key.entity'; +import { + IDatabaseCreateOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseManyOptions, + IDatabaseOptions, +} from 'src/common/database/interfaces/database.interface'; + +export interface IApiKeyService { + findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + + findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise; + + findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise; + + findOneByKey( + key: string, + options?: IDatabaseFindOneOptions + ): Promise; + + findOneByActiveKey( + key: string, + options?: IDatabaseFindOneOptions + ): Promise; + + getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise; + + create( + user: string, + {name, description, startDate, endDate}: ApiKeyCreateDto, + options?: IDatabaseCreateOptions + ): Promise; + + createRaw( + user: string, + {name, description, key, secret, startDate, endDate}: ApiKeyCreateRawDto, + options?: IDatabaseCreateOptions + ): Promise; + + active(repository: ApiKeyDoc): Promise; + + inactive(repository: ApiKeyDoc): Promise; + + update(repository: ApiKeyDoc, data: ApiKeyUpdateDto): Promise; + + updateDate( + repository: ApiKeyDoc, + {startDate, endDate}: ApiKeyUpdateDateDto + ): Promise; + + reset(repository: ApiKeyDoc, secret: string): Promise; + + delete(repository: ApiKeyDoc): Promise; + + validateHashApiKey(hashFromRequest: string, hash: string): Promise; + + createKey(): Promise; + + createSecret(): Promise; + + createHashApiKey(key: string, secret: string): Promise; + + deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise; + + inactiveManyByEndDate(options?: IDatabaseManyOptions): Promise; +} diff --git a/src/common/api-key/repository/api-key.repository.module.ts b/src/common/api-key/repository/api-key.repository.module.ts new file mode 100644 index 0000000..24d7e8d --- /dev/null +++ b/src/common/api-key/repository/api-key.repository.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { + ApiKeyEntity, + ApiKeySchema, +} from 'src/common/api-key/repository/entities/api-key.entity'; +import { ApiKeyRepository } from 'src/common/api-key/repository/repositories/api-key.repository'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; + +@Module({ + providers: [ApiKeyRepository], + exports: [ApiKeyRepository], + controllers: [], + imports: [ + MongooseModule.forFeature( + [ + { + name: ApiKeyEntity.name, + schema: ApiKeySchema, + }, + ], + DATABASE_CONNECTION_NAME + ), + ], +}) +export class ApiKeyRepositoryModule {} diff --git a/src/common/api-key/repository/entities/api-key.entity.ts b/src/common/api-key/repository/entities/api-key.entity.ts new file mode 100644 index 0000000..601908f --- /dev/null +++ b/src/common/api-key/repository/entities/api-key.entity.ts @@ -0,0 +1,85 @@ +import { Prop, SchemaFactory } from '@nestjs/mongoose'; +import { CallbackWithoutResultAndOptionalError } from 'mongoose'; +import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntity } from 'src/common/database/decorators/database.decorator'; +import { Document } from 'mongoose'; +import { UserEntity } from "src/modules/user/repository/entities/user.entity"; + +export const ApiKeyDatabaseName = 'apikeys'; + +@DatabaseEntity({ collection: ApiKeyDatabaseName }) +export class ApiKeyEntity extends DatabaseMongoUUIDEntityAbstract { + @Prop({ + required: true, + ref: UserEntity.name, + index: true, + }) + user: string; + + @Prop({ + required: true, + index: true, + type: String, + minlength: 1, + maxlength: 100, + lowercase: true, + trim: true, + }) + name: string; + + @Prop({ + required: false, + type: String, + minlength: 1, + maxlength: 255, + }) + description?: string; + + @Prop({ + required: true, + type: String, + unique: true, + index: true, + trim: true, + }) + key: string; + + @Prop({ + required: true, + trim: true, + type: String, + }) + hash: string; + + @Prop({ + required: true, + index: true, + type: Boolean, + }) + isActive: boolean; + + @Prop({ + required: false, + type: Date, + }) + startDate?: Date; + + @Prop({ + required: false, + type: Date, + }) + endDate?: Date; +} + +export const ApiKeySchema = SchemaFactory.createForClass(ApiKeyEntity); + +export type ApiKeyDoc = ApiKeyEntity & Document; + +ApiKeySchema.pre( + 'save', + function (next: CallbackWithoutResultAndOptionalError) { + this.name = this.name.toLowerCase(); + + next(); + } +); diff --git a/src/common/api-key/repository/repositories/api-key.repository.ts b/src/common/api-key/repository/repositories/api-key.repository.ts new file mode 100644 index 0000000..b67973b --- /dev/null +++ b/src/common/api-key/repository/repositories/api-key.repository.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { Model } from 'mongoose'; +import { + ApiKeyEntity, + ApiKeyDoc, +} from 'src/common/api-key/repository/entities/api-key.entity'; +import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; +import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; + +@Injectable() +export class ApiKeyRepository extends DatabaseMongoUUIDRepositoryAbstract< + ApiKeyEntity, + ApiKeyDoc +> { + constructor( + @DatabaseModel(ApiKeyEntity.name) + private readonly ApiKeyDoc: Model + ) { + super(ApiKeyDoc); + } +} diff --git a/src/common/api-key/serializations/api-key.create.serialization.ts b/src/common/api-key/serializations/api-key.create.serialization.ts new file mode 100644 index 0000000..185f848 --- /dev/null +++ b/src/common/api-key/serializations/api-key.create.serialization.ts @@ -0,0 +1,14 @@ +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { ApiKeyGetSerialization } from 'src/common/api-key/serializations/api-key.get.serialization'; + +export class ApiKeyCreateSerialization extends PickType( + ApiKeyGetSerialization, + ['key', '_id'] as const +) { + @ApiProperty({ + description: 'Secret key of ApiKey, only show at once', + example: true, + required: true, + }) + secret: string; +} diff --git a/src/common/api-key/serializations/api-key.get.serialization.ts b/src/common/api-key/serializations/api-key.get.serialization.ts new file mode 100644 index 0000000..53d47b3 --- /dev/null +++ b/src/common/api-key/serializations/api-key.get.serialization.ts @@ -0,0 +1,70 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; + +export class ApiKeyGetSerialization extends ResponseIdSerialization { + @ApiProperty({ + description: 'Alias name of api key', + example: faker.name.jobTitle(), + required: true, + }) + name: string; + + @ApiProperty({ + description: 'Description of api key', + example: 'blabla description', + required: false, + }) + description?: string; + + @ApiProperty({ + description: 'Unique key of api key', + example: faker.random.alpha(115), + required: true, + }) + key: string; + + @Exclude() + hash: string; + + @ApiProperty({ + description: 'Active flag of api key', + example: true, + required: true, + }) + isActive: boolean; + + @ApiProperty({ + description: 'Api Key start date', + example: faker.date.recent(), + required: false, + nullable: true, + }) + startDate?: Date; + + @ApiProperty({ + description: 'Api Key end date', + example: faker.date.recent(), + required: false, + nullable: true, + }) + endDate?: Date; + + @ApiProperty({ + description: 'Date created at', + example: faker.date.recent(), + required: false, + }) + readonly createdAt: Date; + + @ApiProperty({ + description: 'Date updated at', + example: faker.date.recent(), + required: false, + }) + readonly updatedAt: Date; + + @Exclude() + readonly deletedAt?: Date; +} diff --git a/src/common/api-key/serializations/api-key.list.serialization.ts b/src/common/api-key/serializations/api-key.list.serialization.ts new file mode 100644 index 0000000..6c940d6 --- /dev/null +++ b/src/common/api-key/serializations/api-key.list.serialization.ts @@ -0,0 +1,6 @@ +import { OmitType } from '@nestjs/swagger'; +import { ApiKeyGetSerialization } from 'src/common/api-key/serializations/api-key.get.serialization'; + +export class ApiKeyListSerialization extends OmitType(ApiKeyGetSerialization, [ + 'description', +] as const) {} diff --git a/src/common/api-key/serializations/api-key.reset.serialization.ts b/src/common/api-key/serializations/api-key.reset.serialization.ts new file mode 100644 index 0000000..2d6b9cd --- /dev/null +++ b/src/common/api-key/serializations/api-key.reset.serialization.ts @@ -0,0 +1,3 @@ +import { ApiKeyCreateSerialization } from 'src/common/api-key/serializations/api-key.create.serialization'; + +export class ApiKeyResetSerialization extends ApiKeyCreateSerialization {} diff --git a/src/common/api-key/services/api-key.service.ts b/src/common/api-key/services/api-key.service.ts new file mode 100644 index 0000000..455c955 --- /dev/null +++ b/src/common/api-key/services/api-key.service.ts @@ -0,0 +1,260 @@ +import { Injectable } from '@nestjs/common'; +import { + IDatabaseCreateOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseManyOptions, + IDatabaseExistOptions, +} from 'src/common/database/interfaces/database.interface'; +import { IApiKeyService } from 'src/common/api-key/interfaces/api-key.service.interface'; +import { IApiKeyCreated } from 'src/common/api-key/interfaces/api-key.interface'; +import { + ApiKeyDoc, + ApiKeyEntity, +} from 'src/common/api-key/repository/entities/api-key.entity'; +import { ApiKeyRepository } from 'src/common/api-key/repository/repositories/api-key.repository'; +import { ApiKeyActiveDto } from 'src/common/api-key/dtos/api-key.active.dto'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; +import { ConfigService } from '@nestjs/config'; +import { HelperHashService } from 'src/common/helper/services/helper.hash.service'; +import { + ApiKeyCreateDto, + ApiKeyCreateRawDto, +} from 'src/common/api-key/dtos/api-key.create.dto'; +import { ApiKeyUpdateDto } from 'src/common/api-key/dtos/api-key.update.dto'; +import { ApiKeyUpdateDateDto } from 'src/common/api-key/dtos/api-key.update-date.dto'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; + +@Injectable() +export class ApiKeyService implements IApiKeyService { + private readonly env: string; + + constructor( + private readonly helperStringService: HelperStringService, + private readonly configService: ConfigService, + private readonly helperHashService: HelperHashService, + private readonly helperDateService: HelperDateService, + private readonly apiKeyRepository: ApiKeyRepository + ) { + this.env = this.configService.get('app.env'); + } + + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.apiKeyRepository.findAll(find, options); + } + + async findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.apiKeyRepository.findOneById(_id, options); + } + + async findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + return this.apiKeyRepository.findOne(find, options); + } + + async existByUser( + user: string, + options?: IDatabaseExistOptions + ): Promise { + return this.apiKeyRepository.exists( + { + user, + }, + options + ); + } + + async findOneByKey( + key: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.apiKeyRepository.findOne({ key }, options); + } + + async findOneByActiveKey( + key: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.apiKeyRepository.findOne( + { + key, + isActive: true, + }, + options + ); + } + + async getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise { + return this.apiKeyRepository.getTotal(find, options); + } + + async create( + user: string, + { name, description, startDate, endDate }: ApiKeyCreateDto, + options?: IDatabaseCreateOptions + ): Promise { + const key = await this.createKey(); + const secret = await this.createSecret(); + const hash: string = await this.createHashApiKey(key, secret); + + const dto: ApiKeyEntity = new ApiKeyEntity(); + dto.user = user; + dto.name = name; + dto.description = description; + dto.key = key; + dto.hash = hash; + dto.isActive = true; + + if (startDate && endDate) { + dto.startDate = startDate; + dto.endDate = endDate; + } + + const created: ApiKeyDoc = + await this.apiKeyRepository.create(dto, options); + + return { doc: created, secret }; + } + + async createRaw( + user: string, + { + name, + description, + key, + secret, + startDate, + endDate, + }: ApiKeyCreateRawDto, + options?: IDatabaseCreateOptions + ): Promise { + const hash: string = await this.createHashApiKey(key, secret); + + const dto: ApiKeyEntity = new ApiKeyEntity(); + dto.name = name; + dto.user = user; + dto.description = description; + dto.key = key; + dto.hash = hash; + dto.isActive = true; + + if (startDate && endDate) { + dto.startDate = this.helperDateService.startOfDay(startDate); + dto.endDate = this.helperDateService.endOfDay(endDate); + } + + const created: ApiKeyDoc = + await this.apiKeyRepository.create(dto, options); + + return { doc: created, secret }; + } + + async active(repository: ApiKeyDoc): Promise { + repository.isActive = true; + + return this.apiKeyRepository.save(repository); + } + + async inactive(repository: ApiKeyDoc): Promise { + repository.isActive = false; + + return this.apiKeyRepository.save(repository); + } + + async update( + repository: ApiKeyDoc, + { name, description }: ApiKeyUpdateDto + ): Promise { + repository.name = name; + repository.description = description; + + return this.apiKeyRepository.save(repository); + } + + async updateDate( + repository: ApiKeyDoc, + { startDate, endDate }: ApiKeyUpdateDateDto + ): Promise { + repository.startDate = this.helperDateService.startOfDay(startDate); + repository.endDate = this.helperDateService.endOfDay(endDate); + + return this.apiKeyRepository.save(repository); + } + + async reset(repository: ApiKeyDoc, secret: string): Promise { + const hash: string = await this.createHashApiKey( + repository.key, + secret + ); + + repository.hash = hash; + + return this.apiKeyRepository.save(repository); + } + + async delete(repository: ApiKeyDoc): Promise { + return this.apiKeyRepository.softDelete(repository); + } + + async validateHashApiKey( + hashFromRequest: string, + hash: string + ): Promise { + return this.helperHashService.sha256Compare(hashFromRequest, hash); + } + + async createKey(): Promise { + return this.helperStringService.random(25, { + safe: false, + upperCase: true, + prefix: `${this.env}_`, + }); + } + + async createSecret(): Promise { + return this.helperStringService.random(35, { + safe: false, + upperCase: true, + }); + } + + async createHashApiKey(key: string, secret: string): Promise { + return this.helperHashService.sha256(`${key}:${secret}`); + } + + async deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise { + return this.apiKeyRepository.deleteMany(find, options); + } + + async inactiveManyByEndDate( + options?: IDatabaseManyOptions + ): Promise { + return this.apiKeyRepository.updateMany( + { + endDate: { + $lte: this.helperDateService.create(), + }, + isActive: true, + }, + { + isActive: false, + }, + options + ); + } +} diff --git a/src/common/api-key/tasks/api-key.inactive.task.ts b/src/common/api-key/tasks/api-key.inactive.task.ts new file mode 100644 index 0000000..2a6a1c0 --- /dev/null +++ b/src/common/api-key/tasks/api-key.inactive.task.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; + +@Injectable() +export class ApiKeyInactiveTask { + constructor(private readonly apiKeyService: ApiKeyService) {} + + @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT, { + name: 'inactiveApiKey', + }) + async inactiveApiKey(): Promise { + try { + await this.apiKeyService.inactiveManyByEndDate(); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } +} diff --git a/src/common/auth/auth.module.ts b/src/common/auth/auth.module.ts new file mode 100644 index 0000000..645ea13 --- /dev/null +++ b/src/common/auth/auth.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AuthJwtAccessStrategy } from 'src/common/auth/guards/jwt-access/auth.jwt-access.strategy'; +import { AuthJwtRefreshStrategy } from 'src/common/auth/guards/jwt-refresh/auth.jwt-refresh.strategy'; +import { AuthService } from 'src/common/auth/services/auth.service'; + +@Module({ + providers: [AuthService, AuthJwtAccessStrategy, AuthJwtRefreshStrategy], + exports: [AuthService], + controllers: [], + imports: [], +}) +export class AuthModule {} diff --git a/src/common/auth/constants/auth.constant.ts b/src/common/auth/constants/auth.constant.ts new file mode 100644 index 0000000..87563cc --- /dev/null +++ b/src/common/auth/constants/auth.constant.ts @@ -0,0 +1,2 @@ +export const AUTH_ACCESS_FOR_META_KEY = 'AuthAccessForMetaKey'; +export const AUTH_PERMISSION_META_KEY = 'AuthPermissionMetaKey'; diff --git a/src/common/auth/constants/auth.enum.constant.ts b/src/common/auth/constants/auth.enum.constant.ts new file mode 100644 index 0000000..8cb673f --- /dev/null +++ b/src/common/auth/constants/auth.enum.constant.ts @@ -0,0 +1,17 @@ +export enum ENUM_AUTH_ACCESS_FOR_SUPER_ADMIN { + SUPER_ADMIN = 'SUPER_ADMIN', +} + +export enum ENUM_AUTH_ACCESS_FOR_DEFAULT { + USER = 'USER', + ADMIN = 'ADMIN', +} + +export const ENUM_AUTH_ACCESS_FOR = { + ...ENUM_AUTH_ACCESS_FOR_SUPER_ADMIN, + ...ENUM_AUTH_ACCESS_FOR_DEFAULT, +}; + +export type ENUM_AUTH_ACCESS_FOR = + | ENUM_AUTH_ACCESS_FOR_SUPER_ADMIN + | ENUM_AUTH_ACCESS_FOR_DEFAULT; diff --git a/src/common/auth/constants/auth.enum.permission.constant.ts b/src/common/auth/constants/auth.enum.permission.constant.ts new file mode 100644 index 0000000..e00f179 --- /dev/null +++ b/src/common/auth/constants/auth.enum.permission.constant.ts @@ -0,0 +1,32 @@ +export enum ENUM_AUTH_PERMISSIONS { + API_KEY_READ = 'API_KEY_READ', + API_KEY_UPDATE = 'API_KEY_UPDATE', + API_KEY_ACTIVE = 'API_KEY_ACTIVE', + API_KEY_INACTIVE = 'API_KEY_INACTIVE', + API_KEY_UPDATE_DATE = 'API_KEY_UPDATE_DATE', + + USER_READ = 'USER_READ', + USER_CREATE = 'USER_CREATE', + USER_UPDATE = 'USER_UPDATE', + USER_ACTIVE = 'USER_ACTIVE', + USER_INACTIVE = 'USER_INACTIVE', + USER_BLOCKED = 'USER_BLOCKED', + USER_DELETE = 'USER_DELETE', + USER_IMPORT = 'USER_IMPORT', + USER_EXPORT = 'USER_EXPORT', + + ROLE_CREATE = 'ROLE_CREATE', + ROLE_UPDATE = 'ROLE_UPDATE', + ROLE_ACTIVE = 'ROLE_ACTIVE', + ROLE_INACTIVE = 'ROLE_INACTIVE', + ROLE_READ = 'ROLE_READ', + ROLE_DELETE = 'ROLE_DELETE', + + PERMISSION_READ = 'PERMISSION_READ', + PERMISSION_UPDATE = 'PERMISSION_UPDATE', + PERMISSION_ACTIVE = 'PERMISSION_ACTIVE', + PERMISSION_INACTIVE = 'PERMISSION_INACTIVE', + + SETTING_READ = 'SETTING_READ', + SETTING_UPDATE = 'SETTING_UPDATE', +} diff --git a/src/common/auth/constants/auth.status-code.constant.ts b/src/common/auth/constants/auth.status-code.constant.ts new file mode 100644 index 0000000..203b6b7 --- /dev/null +++ b/src/common/auth/constants/auth.status-code.constant.ts @@ -0,0 +1,10 @@ +export enum ENUM_AUTH_STATUS_CODE_ERROR { + AUTH_JWT_ACCESS_TOKEN_ERROR = 5100, + AUTH_JWT_REFRESH_TOKEN_ERROR = 5101, + AUTH_PERMISSION_TOKEN_ERROR = 5102, + AUTH_PERMISSION_TOKEN_INVALID_ERROR = 5103, + AUTH_PERMISSION_TOKEN_NOT_YOUR_ERROR = 5104, + AUTH_ROLE_ACCESS_FOR_INVALID_ERROR = 5105, + AUTH_PERMISSION_INVALID_ERROR = 5106, + AUTH_ACCESS_FOR_INVALID_ERROR = 5107, +} diff --git a/src/common/auth/decorators/auth.jwt.decorator.ts b/src/common/auth/decorators/auth.jwt.decorator.ts new file mode 100644 index 0000000..42534ef --- /dev/null +++ b/src/common/auth/decorators/auth.jwt.decorator.ts @@ -0,0 +1,49 @@ +import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'; +import { AUTH_ACCESS_FOR_META_KEY } from 'src/common/auth/constants/auth.constant'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { AuthJwtAccessGuard } from 'src/common/auth/guards/jwt-access/auth.jwt-access.guard'; +import { AuthJwtRefreshGuard } from 'src/common/auth/guards/jwt-refresh/auth.jwt-refresh.guard'; +import { AuthPayloadAccessForGuard } from 'src/common/auth/guards/payload/auth.payload.access-for.guard'; +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const AuthJwtPayload = createParamDecorator( + (data: string, ctx: ExecutionContext): Record => { + const { user } = ctx.switchToHttp().getRequest(); + return data ? user[data] : user; + } +); + +export const AuthJwtToken = createParamDecorator( + (data: string, ctx: ExecutionContext): string => { + const { headers } = ctx.switchToHttp().getRequest(); + const { authorization } = headers; + const authorizations: string[] = authorization.split(' '); + + return authorizations.length >= 2 ? authorizations[1] : undefined; + } +); + +export function AuthJwtAccessProtected(): MethodDecorator { + return applyDecorators(UseGuards(AuthJwtAccessGuard)); +} + +export function AuthJwtPublicAccessProtected(): MethodDecorator { + return applyDecorators( + UseGuards(AuthJwtAccessGuard, AuthPayloadAccessForGuard), + SetMetadata(AUTH_ACCESS_FOR_META_KEY, [ENUM_AUTH_ACCESS_FOR.USER]) + ); +} + +export function AuthJwtAdminAccessProtected(): MethodDecorator { + return applyDecorators( + UseGuards(AuthJwtAccessGuard, AuthPayloadAccessForGuard), + SetMetadata(AUTH_ACCESS_FOR_META_KEY, [ + ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN, + ENUM_AUTH_ACCESS_FOR.ADMIN, + ]) + ); +} + +export function AuthJwtRefreshProtected(): MethodDecorator { + return applyDecorators(UseGuards(AuthJwtRefreshGuard)); +} diff --git a/src/common/auth/decorators/auth.permission.decorator.ts b/src/common/auth/decorators/auth.permission.decorator.ts new file mode 100644 index 0000000..63f6917 --- /dev/null +++ b/src/common/auth/decorators/auth.permission.decorator.ts @@ -0,0 +1,27 @@ +import { + applyDecorators, + createParamDecorator, + ExecutionContext, + SetMetadata, + UseGuards, +} from '@nestjs/common'; +import { AUTH_PERMISSION_META_KEY } from 'src/common/auth/constants/auth.constant'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { AuthPayloadPermissionGuard } from 'src/common/auth/guards/payload/auth.payload.permission.guard'; +import { AuthPermissionGuard } from 'src/common/auth/guards/permission/auth.permission.guard'; + +export const AuthPermissionPayload = createParamDecorator( + (data: string, ctx: ExecutionContext): Record => { + const { permissions } = ctx.switchToHttp().getRequest(); + return permissions; + } +); + +export function AuthPermissionProtected( + ...permissions: ENUM_AUTH_PERMISSIONS[] +): MethodDecorator { + return applyDecorators( + UseGuards(AuthPermissionGuard, AuthPayloadPermissionGuard), + SetMetadata(AUTH_PERMISSION_META_KEY, permissions) + ); +} diff --git a/src/common/auth/guards/jwt-access/auth.jwt-access.guard.ts b/src/common/auth/guards/jwt-access/auth.jwt-access.guard.ts new file mode 100644 index 0000000..96ee540 --- /dev/null +++ b/src/common/auth/guards/jwt-access/auth.jwt-access.guard.ts @@ -0,0 +1,19 @@ +import { AuthGuard } from '@nestjs/passport'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant'; + +@Injectable() +export class AuthJwtAccessGuard extends AuthGuard('jwt') { + handleRequest(err: Error, user: TUser, info: Error): TUser { + if (err || !user) { + throw new UnauthorizedException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_ACCESS_TOKEN_ERROR, + message: 'auth.error.accessTokenUnauthorized', + _error: err ? err.message : info.message, + }); + } + + return user; + } +} diff --git a/src/common/auth/guards/jwt-access/auth.jwt-access.strategy.ts b/src/common/auth/guards/jwt-access/auth.jwt-access.strategy.ts new file mode 100644 index 0000000..5d25968 --- /dev/null +++ b/src/common/auth/guards/jwt-access/auth.jwt-access.strategy.ts @@ -0,0 +1,40 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { AuthService } from 'src/common/auth/services/auth.service'; + +@Injectable() +export class AuthJwtAccessStrategy extends PassportStrategy(Strategy, 'jwt') { + constructor( + private readonly configService: ConfigService, + private readonly authService: AuthService + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme( + configService.get('auth.prefixAuthorization') + ), + ignoreExpiration: false, + jsonWebTokenOptions: { + ignoreNotBefore: false, + audience: configService.get('auth.audience'), + issuer: configService.get('auth.issuer'), + subject: configService.get('auth.subject'), + }, + secretOrKey: configService.get( + 'auth.accessToken.secretKey' + ), + }); + } + + async validate({ + data, + }: Record): Promise> { + const payloadEncryption: boolean = + await this.authService.getPayloadEncryption(); + + return payloadEncryption + ? this.authService.decryptAccessToken({ data }) + : data; + } +} diff --git a/src/common/auth/guards/jwt-refresh/auth.jwt-refresh.guard.ts b/src/common/auth/guards/jwt-refresh/auth.jwt-refresh.guard.ts new file mode 100644 index 0000000..4082b0c --- /dev/null +++ b/src/common/auth/guards/jwt-refresh/auth.jwt-refresh.guard.ts @@ -0,0 +1,19 @@ +import { AuthGuard } from '@nestjs/passport'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant'; + +@Injectable() +export class AuthJwtRefreshGuard extends AuthGuard('jwtRefresh') { + handleRequest(err: Error, user: TUser, info: Error): TUser { + if (err || !user) { + throw new UnauthorizedException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_REFRESH_TOKEN_ERROR, + message: 'auth.error.refreshTokenUnauthorized', + _error: err ? err.message : info.message, + }); + } + + return user; + } +} diff --git a/src/common/auth/guards/jwt-refresh/auth.jwt-refresh.strategy.ts b/src/common/auth/guards/jwt-refresh/auth.jwt-refresh.strategy.ts new file mode 100644 index 0000000..969b790 --- /dev/null +++ b/src/common/auth/guards/jwt-refresh/auth.jwt-refresh.strategy.ts @@ -0,0 +1,43 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { AuthService } from 'src/common/auth/services/auth.service'; + +@Injectable() +export class AuthJwtRefreshStrategy extends PassportStrategy( + Strategy, + 'jwtRefresh' +) { + constructor( + private readonly configService: ConfigService, + private readonly authService: AuthService + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme( + configService.get('auth.prefixAuthorization') + ), + ignoreExpiration: false, + jsonWebTokenOptions: { + ignoreNotBefore: false, + audience: configService.get('auth.audience'), + issuer: configService.get('auth.issuer'), + subject: configService.get('auth.subject'), + }, + secretOrKey: configService.get( + 'auth.refreshToken.secretKey' + ), + }); + } + + async validate({ + data, + }: Record): Promise> { + const payloadEncryption: boolean = + await this.authService.getPayloadEncryption(); + + return payloadEncryption + ? this.authService.decryptRefreshToken({ data }) + : data; + } +} diff --git a/src/common/auth/guards/payload/auth.payload.access-for.guard.ts b/src/common/auth/guards/payload/auth.payload.access-for.guard.ts new file mode 100644 index 0000000..2842d58 --- /dev/null +++ b/src/common/auth/guards/payload/auth.payload.access-for.guard.ts @@ -0,0 +1,48 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AUTH_ACCESS_FOR_META_KEY } from 'src/common/auth/constants/auth.constant'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; + +@Injectable() +export class AuthPayloadAccessForGuard implements CanActivate { + constructor( + private readonly reflector: Reflector, + private readonly helperArrayService: HelperArrayService + ) {} + + async canActivate(context: ExecutionContext): Promise { + const requiredFor: ENUM_AUTH_ACCESS_FOR[] = + this.reflector.getAllAndOverride( + AUTH_ACCESS_FOR_META_KEY, + [context.getHandler(), context.getClass()] + ); + + const { user } = context.switchToHttp().getRequest(); + const { accessFor } = user; + + if (!requiredFor || accessFor === ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN) { + return true; + } + + const hasFor: boolean = this.helperArrayService.includes( + requiredFor, + accessFor + ); + + if (!hasFor) { + throw new ForbiddenException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_ACCESS_FOR_INVALID_ERROR, + message: 'auth.error.accessForForbidden', + }); + } + return hasFor; + } +} diff --git a/src/common/auth/guards/payload/auth.payload.permission.guard.ts b/src/common/auth/guards/payload/auth.payload.permission.guard.ts new file mode 100644 index 0000000..705a1da --- /dev/null +++ b/src/common/auth/guards/payload/auth.payload.permission.guard.ts @@ -0,0 +1,57 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, + UnauthorizedException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AUTH_PERMISSION_META_KEY } from 'src/common/auth/constants/auth.constant'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; + +@Injectable() +export class AuthPayloadPermissionGuard implements CanActivate { + constructor( + private readonly reflector: Reflector, + private readonly helperArrayService: HelperArrayService + ) {} + + async canActivate(context: ExecutionContext): Promise { + const requiredPermission: ENUM_AUTH_PERMISSIONS[] = + this.reflector.getAllAndOverride( + AUTH_PERMISSION_META_KEY, + [context.getHandler(), context.getClass()] + ); + + const { permissions, user } = context.switchToHttp().getRequest(); + if (!user) { + throw new UnauthorizedException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_ACCESS_TOKEN_ERROR, + message: 'auth.error.accessTokenUnauthorized', + }); + } else if ( + !requiredPermission || + user.accessFor === ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN + ) { + return true; + } + + const hasPermission: boolean = this.helperArrayService.in( + permissions, + requiredPermission + ); + + if (!hasPermission) { + throw new ForbiddenException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_INVALID_ERROR, + message: 'auth.error.permissionForbidden', + }); + } + return hasPermission; + } +} diff --git a/src/common/auth/guards/permission/auth.permission.guard.ts b/src/common/auth/guards/permission/auth.permission.guard.ts new file mode 100644 index 0000000..1ed14d2 --- /dev/null +++ b/src/common/auth/guards/permission/auth.permission.guard.ts @@ -0,0 +1,81 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, + ForbiddenException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant'; +import { AuthService } from 'src/common/auth/services/auth.service'; + +@Injectable() +export class AuthPermissionGuard implements CanActivate { + private readonly headerName: string; + + constructor( + private readonly configService: ConfigService, + private readonly authService: AuthService + ) { + this.headerName = this.configService.get( + 'auth.permissionToken.headerName' + ); + } + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { user } = request; + + if (!user) { + throw new UnauthorizedException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_ACCESS_TOKEN_ERROR, + message: 'auth.error.accessTokenUnauthorized', + }); + } else if (user.accessFor === ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN) { + return true; + } + + const { [this.headerName]: token } = request.headers; + + if (!token) { + throw new UnauthorizedException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_ERROR, + message: 'auth.error.permissionTokenUnauthorized', + }); + } + + const validate: boolean = + await this.authService.validatePermissionToken(token); + if (!validate) { + throw new UnauthorizedException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_INVALID_ERROR, + message: 'auth.error.permissionTokenInvalid', + }); + } + + const { data } = await this.authService.payloadPermissionToken(token); + + const payloadEncryption = await this.authService.getPayloadEncryption(); + let payloadDecryptPermissionToken: Record = data; + + if (payloadEncryption) { + payloadDecryptPermissionToken = + await this.authService.decryptPermissionToken(data); + } + + if (payloadDecryptPermissionToken._id !== user._id) { + throw new ForbiddenException({ + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_NOT_YOUR_ERROR, + message: 'auth.error.permissionTokenNotYour', + }); + } + request.permissions = payloadDecryptPermissionToken.permissions; + + return true; + } +} diff --git a/src/common/auth/interfaces/auth.interface.ts b/src/common/auth/interfaces/auth.interface.ts new file mode 100644 index 0000000..13865e9 --- /dev/null +++ b/src/common/auth/interfaces/auth.interface.ts @@ -0,0 +1,17 @@ +// Auth +export interface IAuthPassword { + salt: string; + passwordHash: string; + passwordExpired: Date; + passwordCreated: Date; +} + +export interface IAuthPayloadOptions { + loginDate: Date; +} + +export interface IAuthRefreshTokenOptions { + // in milis + notBeforeExpirationTime?: number | string; + rememberMe?: boolean; +} diff --git a/src/common/auth/interfaces/auth.service.interface.ts b/src/common/auth/interfaces/auth.service.interface.ts new file mode 100644 index 0000000..dd91466 --- /dev/null +++ b/src/common/auth/interfaces/auth.service.interface.ts @@ -0,0 +1,93 @@ +import { + IAuthPassword, + IAuthPayloadOptions, + IAuthRefreshTokenOptions, +} from 'src/common/auth/interfaces/auth.interface'; + +export interface IAuthService { + encryptAccessToken(payload: Record): Promise; + + decryptAccessToken( + payload: Record + ): Promise>; + + createAccessToken( + payloadHashed: string | Record + ): Promise; + + validateAccessToken(token: string): Promise; + + payloadAccessToken(token: string): Promise>; + + encryptRefreshToken(payload: Record): Promise; + + decryptRefreshToken( + payload: Record + ): Promise>; + + createRefreshToken( + payloadHashed: string | Record, + options?: IAuthRefreshTokenOptions + ): Promise; + + validateRefreshToken(token: string): Promise; + + payloadRefreshToken(token: string): Promise>; + + encryptPermissionToken(payload: Record): Promise; + + decryptPermissionToken({ + data, + }: Record): Promise>; + + createPermissionToken( + payloadHashed: string | Record + ): Promise; + + validatePermissionToken(token: string): Promise; + + payloadPermissionToken(token: string): Promise>; + + validateUser( + passwordString: string, + passwordHash: string + ): Promise; + + createPayloadAccessToken( + data: Record, + rememberMe: boolean, + options?: IAuthPayloadOptions + ): Promise>; + + createPayloadRefreshToken( + _id: string, + rememberMe: boolean, + options?: IAuthPayloadOptions + ): Promise>; + + createPayloadPermissionToken( + data: Record + ): Promise>; + + createSalt(length: number): Promise; + + createPassword(password: string): Promise; + + checkPasswordExpired(passwordExpired: Date): Promise; + + getTokenType(): Promise; + + getAccessTokenExpirationTime(): Promise; + + getRefreshTokenExpirationTime(rememberMe?: boolean): Promise; + + getIssuer(): Promise; + + getAudience(): Promise; + + getSubject(): Promise; + + getPayloadEncryption(): Promise; + + getPermissionTokenExpirationTime(): Promise; +} diff --git a/src/common/auth/services/auth.service.ts b/src/common/auth/services/auth.service.ts new file mode 100644 index 0000000..2d8c938 --- /dev/null +++ b/src/common/auth/services/auth.service.ts @@ -0,0 +1,371 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + IAuthPassword, + IAuthPayloadOptions, + IAuthRefreshTokenOptions, +} from 'src/common/auth/interfaces/auth.interface'; +import { IAuthService } from 'src/common/auth/interfaces/auth.service.interface'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { HelperEncryptionService } from 'src/common/helper/services/helper.encryption.service'; +import { HelperHashService } from 'src/common/helper/services/helper.hash.service'; + +@Injectable() +export class AuthService implements IAuthService { + private readonly accessTokenSecretKey: string; + private readonly accessTokenExpirationTime: number; + private readonly accessTokenNotBeforeExpirationTime: number; + private readonly accessTokenEncryptKey: string; + private readonly accessTokenEncryptIv: string; + + private readonly refreshTokenSecretKey: string; + private readonly refreshTokenExpirationTime: number; + private readonly refreshTokenExpirationTimeRememberMe: number; + private readonly refreshTokenNotBeforeExpirationTime: number; + private readonly refreshTokenEncryptKey: string; + private readonly refreshTokenEncryptIv: string; + + private readonly payloadEncryption: boolean; + private readonly prefixAuthorization: string; + private readonly audience: string; + private readonly issuer: string; + private readonly subject: string; + + private readonly passwordExpiredIn: number; + private readonly passwordSaltLength: number; + + private readonly permissionTokenSecretToken: string; + private readonly permissionTokenExpirationTime: number; + private readonly permissionTokenNotBeforeExpirationTime: number; + private readonly permissionTokenEncryptKey: string; + private readonly permissionTokenEncryptIv: string; + + constructor( + private readonly helperHashService: HelperHashService, + private readonly helperDateService: HelperDateService, + private readonly helperEncryptionService: HelperEncryptionService, + private readonly configService: ConfigService + ) { + this.accessTokenSecretKey = this.configService.get( + 'auth.accessToken.secretKey' + ); + this.accessTokenExpirationTime = this.configService.get( + 'auth.accessToken.expirationTime' + ); + this.accessTokenNotBeforeExpirationTime = + this.configService.get( + 'auth.accessToken.notBeforeExpirationTime' + ); + this.accessTokenEncryptKey = this.configService.get( + 'auth.accessToken.encryptKey' + ); + this.accessTokenEncryptIv = this.configService.get( + 'auth.accessToken.encryptIv' + ); + + this.refreshTokenSecretKey = this.configService.get( + 'auth.refreshToken.secretKey' + ); + this.refreshTokenExpirationTime = this.configService.get( + 'auth.refreshToken.expirationTime' + ); + this.refreshTokenExpirationTimeRememberMe = + this.configService.get( + 'auth.refreshToken.expirationTimeRememberMe' + ); + this.refreshTokenNotBeforeExpirationTime = + this.configService.get( + 'auth.refreshToken.notBeforeExpirationTime' + ); + this.refreshTokenEncryptKey = this.configService.get( + 'auth.refreshToken.encryptKey' + ); + this.refreshTokenEncryptIv = this.configService.get( + 'auth.refreshToken.encryptIv' + ); + + this.payloadEncryption = this.configService.get( + 'auth.payloadEncryption' + ); + this.prefixAuthorization = this.configService.get( + 'auth.prefixAuthorization' + ); + this.subject = this.configService.get('auth.subject'); + this.audience = this.configService.get('auth.audience'); + this.issuer = this.configService.get('auth.issuer'); + + this.passwordExpiredIn = this.configService.get( + 'auth.password.expiredIn' + ); + this.passwordSaltLength = this.configService.get( + 'auth.password.saltLength' + ); + + this.permissionTokenSecretToken = this.configService.get( + 'auth.permissionToken.secretKey' + ); + this.permissionTokenExpirationTime = this.configService.get( + 'auth.permissionToken.expirationTime' + ); + this.permissionTokenNotBeforeExpirationTime = + this.configService.get( + 'auth.permissionToken.notBeforeExpirationTime' + ); + this.permissionTokenEncryptKey = this.configService.get( + 'auth.permissionToken.encryptKey' + ); + this.permissionTokenEncryptIv = this.configService.get( + 'auth.permissionToken.encryptIv' + ); + } + + async createSalt(length: number): Promise { + return this.helperHashService.randomSalt(length); + } + + async encryptAccessToken(payload: Record): Promise { + return this.helperEncryptionService.aes256Encrypt( + payload, + this.accessTokenEncryptKey, + this.accessTokenEncryptIv + ); + } + + async decryptAccessToken({ + data, + }: Record): Promise> { + return this.helperEncryptionService.aes256Decrypt( + data, + this.accessTokenEncryptKey, + this.accessTokenEncryptIv + ) as Record; + } + + async createAccessToken( + payloadHashed: string | Record + ): Promise { + return this.helperEncryptionService.jwtEncrypt( + { data: payloadHashed }, + { + secretKey: this.accessTokenSecretKey, + expiredIn: this.accessTokenExpirationTime, + notBefore: this.accessTokenNotBeforeExpirationTime, + audience: this.audience, + issuer: this.issuer, + subject: this.subject, + } + ); + } + + async validateAccessToken(token: string): Promise { + return this.helperEncryptionService.jwtVerify(token, { + secretKey: this.accessTokenSecretKey, + audience: this.audience, + issuer: this.issuer, + subject: this.subject, + }); + } + + async payloadAccessToken(token: string): Promise> { + return this.helperEncryptionService.jwtDecrypt(token); + } + + async encryptRefreshToken(payload: Record): Promise { + return this.helperEncryptionService.aes256Encrypt( + payload, + this.refreshTokenEncryptKey, + this.refreshTokenEncryptIv + ); + } + + async decryptRefreshToken({ + data, + }: Record): Promise> { + return this.helperEncryptionService.aes256Decrypt( + data, + this.refreshTokenEncryptKey, + this.refreshTokenEncryptIv + ) as Record; + } + + async createRefreshToken( + payloadHashed: string | Record, + options?: IAuthRefreshTokenOptions + ): Promise { + return this.helperEncryptionService.jwtEncrypt( + { data: payloadHashed }, + { + secretKey: this.refreshTokenSecretKey, + expiredIn: options?.rememberMe + ? this.refreshTokenExpirationTimeRememberMe + : this.refreshTokenExpirationTime, + notBefore: + options?.notBeforeExpirationTime ?? + this.refreshTokenNotBeforeExpirationTime, + audience: this.audience, + issuer: this.issuer, + subject: this.subject, + } + ); + } + + async validateRefreshToken(token: string): Promise { + return this.helperEncryptionService.jwtVerify(token, { + secretKey: this.refreshTokenSecretKey, + audience: this.audience, + issuer: this.issuer, + subject: this.subject, + }); + } + + async payloadRefreshToken(token: string): Promise> { + return this.helperEncryptionService.jwtDecrypt(token); + } + + async encryptPermissionToken( + payload: Record + ): Promise { + return this.helperEncryptionService.aes256Encrypt( + payload, + this.permissionTokenEncryptKey, + this.permissionTokenEncryptIv + ); + } + + async decryptPermissionToken({ + data, + }: Record): Promise> { + return this.helperEncryptionService.aes256Decrypt( + data, + this.permissionTokenEncryptKey, + this.permissionTokenEncryptIv + ) as Record; + } + + async createPermissionToken( + payloadHashed: string | Record + ): Promise { + return this.helperEncryptionService.jwtEncrypt( + { data: payloadHashed }, + { + secretKey: this.permissionTokenSecretToken, + expiredIn: this.permissionTokenExpirationTime, + notBefore: this.permissionTokenNotBeforeExpirationTime, + audience: this.audience, + issuer: this.issuer, + subject: this.subject, + } + ); + } + + async validatePermissionToken(token: string): Promise { + return this.helperEncryptionService.jwtVerify(token, { + secretKey: this.permissionTokenSecretToken, + audience: this.audience, + issuer: this.issuer, + subject: this.subject, + }); + } + + async payloadPermissionToken(token: string): Promise> { + return this.helperEncryptionService.jwtDecrypt(token); + } + + async validateUser( + passwordString: string, + passwordHash: string + ): Promise { + return this.helperHashService.bcryptCompare( + passwordString, + passwordHash + ); + } + + async createPayloadAccessToken( + data: Record, + rememberMe: boolean, + options?: IAuthPayloadOptions + ): Promise> { + return { + ...data, + rememberMe, + loginDate: options?.loginDate ?? this.helperDateService.create(), + }; + } + + async createPayloadRefreshToken( + _id: string, + rememberMe: boolean, + options?: IAuthPayloadOptions + ): Promise> { + return { + _id, + rememberMe, + loginDate: options?.loginDate, + }; + } + + async createPayloadPermissionToken( + data: Record + ): Promise> { + return data; + } + + async createPassword(password: string): Promise { + const salt: string = this.helperHashService.randomSalt(20); + + const passwordExpired: Date = this.helperDateService.forwardInSeconds( + this.passwordExpiredIn + ); + const passwordCreated: Date = this.helperDateService.create(); + const passwordHash = this.helperHashService.bcrypt(password, salt); + return { + passwordHash, + passwordExpired, + passwordCreated, + salt, + }; + } + + async checkPasswordExpired(passwordExpired: Date): Promise { + const today: Date = this.helperDateService.create(); + const passwordExpiredConvert: Date = + this.helperDateService.create(passwordExpired); + + return today > passwordExpiredConvert; + } + + async getTokenType(): Promise { + return this.prefixAuthorization; + } + + async getAccessTokenExpirationTime(): Promise { + return this.accessTokenExpirationTime; + } + + async getRefreshTokenExpirationTime(rememberMe?: boolean): Promise { + return rememberMe + ? this.refreshTokenExpirationTimeRememberMe + : this.refreshTokenExpirationTime; + } + + async getIssuer(): Promise { + return this.issuer; + } + + async getAudience(): Promise { + return this.audience; + } + + async getSubject(): Promise { + return this.subject; + } + + async getPayloadEncryption(): Promise { + return this.payloadEncryption; + } + + async getPermissionTokenExpirationTime(): Promise { + return this.permissionTokenExpirationTime; + } +} diff --git a/src/common/aws/aws.module.ts b/src/common/aws/aws.module.ts new file mode 100644 index 0000000..bcd9794 --- /dev/null +++ b/src/common/aws/aws.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AwsS3Service } from './services/aws.s3.service'; + +@Module({ + exports: [AwsS3Service], + providers: [AwsS3Service], + imports: [], + controllers: [], +}) +export class AwsModule {} diff --git a/src/common/aws/constants/aws.s3.constant.ts b/src/common/aws/constants/aws.s3.constant.ts new file mode 100644 index 0000000..23ac4a9 --- /dev/null +++ b/src/common/aws/constants/aws.s3.constant.ts @@ -0,0 +1 @@ +export const AwsS3MaxPartNumber = 10000; diff --git a/src/common/aws/interfaces/aws.interface.ts b/src/common/aws/interfaces/aws.interface.ts new file mode 100644 index 0000000..e907cbf --- /dev/null +++ b/src/common/aws/interfaces/aws.interface.ts @@ -0,0 +1,6 @@ +import { ObjectCannedACL } from '@aws-sdk/client-s3'; + +export interface IAwsS3PutItemOptions { + path: string; + acl?: ObjectCannedACL; +} diff --git a/src/common/aws/interfaces/aws.s3-service.interface.ts b/src/common/aws/interfaces/aws.s3-service.interface.ts new file mode 100644 index 0000000..ad417aa --- /dev/null +++ b/src/common/aws/interfaces/aws.s3-service.interface.ts @@ -0,0 +1,63 @@ +import { + CompletedPart, + HeadBucketCommandOutput, + UploadPartRequest, +} from '@aws-sdk/client-s3'; +import { IAwsS3PutItemOptions } from 'src/common/aws/interfaces/aws.interface'; +import { + AwsS3MultipartPartsSerialization, + AwsS3MultipartSerialization, +} from 'src/common/aws/serializations/aws.s3-multipart.serialization'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { Readable } from 'stream'; + +export interface IAwsS3Service { + checkConnection(): Promise; + + listBucket(): Promise; + + listItemInBucket(prefix?: string): Promise; + + getItemInBucket( + filename: string, + path?: string + ): Promise | Blob>; + + putItemInBucket( + filename: string, + content: + | string + | Uint8Array + | Buffer + | Readable + | ReadableStream + | Blob, + options?: IAwsS3PutItemOptions + ): Promise; + + deleteItemInBucket(filename: string): Promise; + + deleteItemsInBucket(filenames: string[]): Promise; + + deleteFolder(dir: string): Promise; + + createMultiPart( + filename: string, + options?: IAwsS3PutItemOptions + ): Promise; + + uploadPart( + path: string, + content: UploadPartRequest['Body'] | string | Uint8Array | Buffer, + uploadId: string, + partNumber: number + ): Promise; + + completeMultipart( + path: string, + uploadId: string, + parts: CompletedPart[] + ): Promise; + + abortMultipart(path: string, uploadId: string): Promise; +} diff --git a/src/common/aws/serializations/aws.s3-multipart.serialization.ts b/src/common/aws/serializations/aws.s3-multipart.serialization.ts new file mode 100644 index 0000000..af29cb8 --- /dev/null +++ b/src/common/aws/serializations/aws.s3-multipart.serialization.ts @@ -0,0 +1,53 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; + +export class AwsS3MultipartPartsSerialization { + @ApiProperty({ + example: faker.random.alpha(10), + description: 'ETag from aws after init multipart', + }) + @Type(() => String) + ETag: string; + + @ApiProperty({ + example: 1, + }) + @Type(() => Number) + PartNumber: number; +} + +export class AwsS3MultipartSerialization extends AwsS3Serialization { + @ApiProperty({ + example: faker.random.alpha(20), + description: 'Upload id from aws after init multipart', + }) + @Type(() => String) + uploadId: string; + + @ApiProperty({ + example: 1, + description: 'Last part number uploaded', + }) + @Type(() => Number) + partNumber?: number; + + @ApiProperty({ + example: 200, + description: 'Max part number, or length of the chunk', + }) + @Type(() => Number) + maxPartNumber?: number; + + @ApiProperty({ + oneOf: [ + { + $ref: getSchemaPath(AwsS3MultipartPartsSerialization), + type: 'array', + }, + ], + }) + @Type(() => AwsS3MultipartPartsSerialization) + parts?: AwsS3MultipartPartsSerialization[]; +} diff --git a/src/common/aws/serializations/aws.s3.serialization.ts b/src/common/aws/serializations/aws.s3.serialization.ts new file mode 100644 index 0000000..9440ebf --- /dev/null +++ b/src/common/aws/serializations/aws.s3.serialization.ts @@ -0,0 +1,41 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +export class AwsS3Serialization { + @ApiProperty({ + example: faker.system.directoryPath(), + }) + @Type(() => String) + path: string; + + @ApiProperty({ + example: faker.system.filePath(), + }) + @Type(() => String) + pathWithFilename: string; + + @ApiProperty({ + example: faker.system.fileName(), + }) + @Type(() => String) + filename: string; + + @ApiProperty({ + example: `${faker.internet.url()}/${faker.system.filePath()}`, + }) + @Type(() => String) + completedUrl: string; + + @ApiProperty({ + example: faker.internet.url(), + }) + @Type(() => String) + baseUrl: string; + + @ApiProperty({ + example: faker.system.mimeType(), + }) + @Type(() => String) + mime: string; +} diff --git a/src/common/aws/services/aws.s3.service.ts b/src/common/aws/services/aws.s3.service.ts new file mode 100644 index 0000000..fc36121 --- /dev/null +++ b/src/common/aws/services/aws.s3.service.ts @@ -0,0 +1,377 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { IAwsS3PutItemOptions } from 'src/common/aws/interfaces/aws.interface'; +import { IAwsS3Service } from 'src/common/aws/interfaces/aws.s3-service.interface'; +import { + AwsS3MultipartPartsSerialization, + AwsS3MultipartSerialization, +} from 'src/common/aws/serializations/aws.s3-multipart.serialization'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { Readable } from 'stream'; +import { + S3Client, + GetObjectCommand, + ListBucketsCommand, + ListObjectsV2Command, + PutObjectCommand, + DeleteObjectCommand, + DeleteObjectsCommand, + ObjectIdentifier, + CreateMultipartUploadCommand, + CreateMultipartUploadCommandInput, + UploadPartCommandInput, + UploadPartCommand, + CompleteMultipartUploadCommandInput, + CompleteMultipartUploadCommand, + CompletedPart, + GetObjectCommandInput, + AbortMultipartUploadCommand, + AbortMultipartUploadCommandInput, + UploadPartRequest, + HeadBucketCommand, + HeadBucketCommandOutput, + ListBucketsOutput, + Bucket, + ListObjectsV2Output, + _Object, + GetObjectOutput, +} from '@aws-sdk/client-s3'; + +@Injectable() +export class AwsS3Service implements IAwsS3Service { + private readonly s3Client: S3Client; + private readonly bucket: string; + private readonly baseUrl: string; + + constructor(private readonly configService: ConfigService) { + this.s3Client = new S3Client({ + credentials: { + accessKeyId: + this.configService.get('aws.credential.key'), + secretAccessKey: this.configService.get( + 'aws.credential.secret' + ), + }, + region: this.configService.get('aws.s3.region'), + }); + + this.bucket = this.configService.get('aws.s3.bucket'); + this.baseUrl = this.configService.get('aws.s3.baseUrl'); + } + + async checkConnection(): Promise { + const command: HeadBucketCommand = new HeadBucketCommand({ + Bucket: this.bucket, + }); + + try { + const check: HeadBucketCommandOutput = await this.s3Client.send( + command + ); + + return check; + } catch (err: any) { + throw err; + } + } + + async listBucket(): Promise { + const command: ListBucketsCommand = new ListBucketsCommand({}); + + try { + const listBucket: ListBucketsOutput = await this.s3Client.send( + command + ); + const mapList = listBucket.Buckets.map((val: Bucket) => val.Name); + + return mapList; + } catch (err: any) { + throw err; + } + } + + async listItemInBucket(prefix?: string): Promise { + const command: ListObjectsV2Command = new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: prefix, + }); + + try { + const listItems: ListObjectsV2Output = await this.s3Client.send( + command + ); + + const mapList = listItems.Contents.map((val: _Object) => { + const lastIndex: number = val.Key.lastIndexOf('/'); + const path: string = val.Key.substring(0, lastIndex); + const filename: string = val.Key.substring( + lastIndex, + val.Key.length + ); + const mime: string = filename + .substring(filename.lastIndexOf('.') + 1, filename.length) + .toLocaleUpperCase(); + + return { + path, + pathWithFilename: val.Key, + filename: filename, + completedUrl: `${this.baseUrl}/${val.Key}`, + baseUrl: this.baseUrl, + mime, + }; + }); + + return mapList; + } catch (err: any) { + throw err; + } + } + + async getItemInBucket( + filename: string, + path?: string + ): Promise | Blob> { + if (path) + path = path.startsWith('/') ? path.replace('/', '') : `${path}`; + + const key: string = path ? `${path}/${filename}` : filename; + const input: GetObjectCommandInput = { + Bucket: this.bucket, + Key: key, + }; + const command: GetObjectCommand = new GetObjectCommand(input); + + try { + const item: GetObjectOutput = await this.s3Client.send(command); + + return item.Body; + } catch (err: any) { + throw err; + } + } + + async putItemInBucket( + filename: string, + content: + | string + | Uint8Array + | Buffer + | Readable + | ReadableStream + | Blob, + options?: IAwsS3PutItemOptions + ): Promise { + let path: string = options?.path; + const acl: string = options?.acl ? options.acl : 'public-read'; + + if (path) + path = path.startsWith('/') ? path.replace('/', '') : `${path}`; + + const mime: string = filename + .substring(filename.lastIndexOf('.') + 1, filename.length) + .toUpperCase(); + const key: string = path ? `${path}/${filename}` : filename; + const command: PutObjectCommand = new PutObjectCommand({ + Bucket: this.bucket, + Key: key, + Body: content, + ACL: acl, + }); + + try { + await this.s3Client.send(command); + } catch (err: any) { + throw err; + } + + return { + path, + pathWithFilename: key, + filename: filename, + completedUrl: `${this.baseUrl}/${key}`, + baseUrl: this.baseUrl, + mime, + }; + } + + async deleteItemInBucket(filename: string): Promise { + const command: DeleteObjectCommand = new DeleteObjectCommand({ + Bucket: this.bucket, + Key: filename, + }); + + try { + await this.s3Client.send(command); + return; + } catch (err: any) { + throw err; + } + } + + async deleteItemsInBucket(filenames: string[]): Promise { + const keys: ObjectIdentifier[] = filenames.map((val) => ({ + Key: val, + })); + const command: DeleteObjectsCommand = new DeleteObjectsCommand({ + Bucket: this.bucket, + Delete: { + Objects: keys, + }, + }); + + try { + await this.s3Client.send(command); + return; + } catch (err: any) { + throw err; + } + } + + async deleteFolder(dir: string): Promise { + const commandList: ListObjectsV2Command = new ListObjectsV2Command({ + Bucket: this.bucket, + Prefix: dir, + }); + const lists = await this.s3Client.send(commandList); + + try { + const listItems = lists.Contents.map((val) => ({ + Key: val.Key, + })); + const commandDeleteItems: DeleteObjectsCommand = + new DeleteObjectsCommand({ + Bucket: this.bucket, + Delete: { + Objects: listItems, + }, + }); + + await this.s3Client.send(commandDeleteItems); + + const commandDelete: DeleteObjectCommand = new DeleteObjectCommand({ + Bucket: this.bucket, + Key: dir, + }); + await this.s3Client.send(commandDelete); + + return; + } catch (err: any) { + throw err; + } + } + + async createMultiPart( + filename: string, + options?: IAwsS3PutItemOptions + ): Promise { + let path: string = options?.path; + const acl: string = options?.acl ? options.acl : 'public-read'; + + if (path) + path = path.startsWith('/') ? path.replace('/', '') : `${path}`; + + const mime: string = filename + .substring(filename.lastIndexOf('.') + 1, filename.length) + .toUpperCase(); + const key: string = path ? `${path}/${filename}` : filename; + + const multiPartInput: CreateMultipartUploadCommandInput = { + Bucket: this.bucket, + Key: key, + ACL: acl, + }; + const multiPartCommand: CreateMultipartUploadCommand = + new CreateMultipartUploadCommand(multiPartInput); + + try { + const response = await this.s3Client.send(multiPartCommand); + + return { + uploadId: response.UploadId, + path, + pathWithFilename: key, + filename: filename, + completedUrl: `${this.baseUrl}/${key}`, + baseUrl: this.baseUrl, + mime, + }; + } catch (err: any) { + throw err; + } + } + + async uploadPart( + path: string, + content: UploadPartRequest['Body'] | string | Uint8Array | Buffer, + uploadId: string, + partNumber: number + ): Promise { + const uploadPartInput: UploadPartCommandInput = { + Bucket: this.bucket, + Key: path, + Body: content, + PartNumber: partNumber, + UploadId: uploadId, + }; + const uploadPartCommand: UploadPartCommand = new UploadPartCommand( + uploadPartInput + ); + + try { + const { ETag } = await this.s3Client.send(uploadPartCommand); + + return { + ETag, + PartNumber: partNumber, + }; + } catch (err: any) { + throw err; + } + } + + async completeMultipart( + path: string, + uploadId: string, + parts: CompletedPart[] + ): Promise { + const completeMultipartInput: CompleteMultipartUploadCommandInput = { + Bucket: this.bucket, + Key: path, + UploadId: uploadId, + MultipartUpload: { + Parts: parts, + }, + }; + + const completeMultipartCommand: CompleteMultipartUploadCommand = + new CompleteMultipartUploadCommand(completeMultipartInput); + + try { + await this.s3Client.send(completeMultipartCommand); + + return; + } catch (err: any) { + throw err; + } + } + + async abortMultipart(path: string, uploadId: string): Promise { + const abortMultipartInput: AbortMultipartUploadCommandInput = { + Bucket: this.bucket, + Key: path, + UploadId: uploadId, + }; + + const abortMultipartCommand: AbortMultipartUploadCommand = + new AbortMultipartUploadCommand(abortMultipartInput); + + try { + await this.s3Client.send(abortMultipartCommand); + + return; + } catch (err: any) { + throw err; + } + } +} diff --git a/src/common/common.module.ts b/src/common/common.module.ts new file mode 100644 index 0000000..f8b6095 --- /dev/null +++ b/src/common/common.module.ts @@ -0,0 +1,179 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { DebuggerModule } from 'src/common/debugger/debugger.module'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { ErrorModule } from 'src/common/error/error.module'; +import { ResponseModule } from 'src/common/response/response.module'; +import { RequestModule } from 'src/common/request/request.module'; +import { AuthModule } from 'src/common/auth/auth.module'; +import { MessageModule } from 'src/common/message/message.module'; +import { LoggerModule } from 'src/common/logger/logger.module'; +import { PaginationModule } from 'src/common/pagination/pagination.module'; +import Joi from 'joi'; +import { ENUM_MESSAGE_LANGUAGE } from './message/constants/message.enum.constant'; +import configs from 'src/configs'; +import { SettingModule } from 'src/common/setting/setting.module'; +import { ApiKeyModule } from 'src/common/api-key/api-key.module'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DatabaseOptionsService } from 'src/common/database/services/database.options.service'; +import { DatabaseOptionsModule } from 'src/common/database/database.options.module'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant'; +import { APP_LANGUAGE } from 'src/app/constants/app.constant'; + +@Module({ + controllers: [], + providers: [], + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + validationSchema: Joi.object({ + APP_NAME: Joi.string().required(), + APP_ENV: Joi.string() + .valid(...Object.values(ENUM_APP_ENVIRONMENT)) + .default('development') + .required(), + APP_LANGUAGE: Joi.string() + .valid(...Object.values(ENUM_MESSAGE_LANGUAGE)) + .default(APP_LANGUAGE) + .required(), + + HTTP_ENABLE: Joi.boolean().default(true).required(), + HTTP_HOST: [ + Joi.string().ip({ version: 'ipv4' }).required(), + Joi.valid('localhost').required(), + ], + HTTP_PORT: Joi.number().default(3000).required(), + HTTP_VERSIONING_ENABLE: Joi.boolean().default(true).required(), + HTTP_VERSION: Joi.number().required(), + + DEBUGGER_HTTP_WRITE_INTO_FILE: Joi.boolean() + .default(false) + .required(), + DEBUGGER_HTTP_WRITE_INTO_CONSOLE: Joi.boolean() + .default(false) + .required(), + DEBUGGER_SYSTEM_WRITE_INTO_FILE: Joi.boolean() + .default(false) + .required(), + DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE: Joi.boolean() + .default(false) + .required(), + + JOB_ENABLE: Joi.boolean().default(false).required(), + + DATABASE_HOST: Joi.string() + .default('mongodb://localhost:27017') + .required(), + DATABASE_NAME: Joi.string().default('ack').required(), + DATABASE_USER: Joi.string().allow(null, '').optional(), + DATABASE_PASSWORD: Joi.string().allow(null, '').optional(), + DATABASE_DEBUG: Joi.boolean().default(false).required(), + DATABASE_OPTIONS: Joi.string().allow(null, '').optional(), + + AUTH_JWT_SUBJECT: Joi.string().required(), + AUTH_JWT_AUDIENCE: Joi.string().required(), + AUTH_JWT_ISSUER: Joi.string().required(), + + AUTH_JWT_ACCESS_TOKEN_SECRET_KEY: Joi.string() + .alphanum() + .min(5) + .max(50) + .required(), + AUTH_JWT_ACCESS_TOKEN_EXPIRED: Joi.string() + .default('15m') + .required(), + + AUTH_JWT_REFRESH_TOKEN_SECRET_KEY: Joi.string() + .alphanum() + .min(5) + .max(50) + .required(), + AUTH_JWT_REFRESH_TOKEN_EXPIRED: Joi.string() + .default('7d') + .required(), + AUTH_JWT_REFRESH_TOKEN_REMEMBER_ME_EXPIRED: Joi.string() + .default('30d') + .required(), + AUTH_JWT_REFRESH_TOKEN_NOT_BEFORE_EXPIRATION: Joi.string() + .default('15m') + .required(), + + AUTH_PERMISSION_TOKEN_SECRET_KEY: Joi.string() + .alphanum() + .min(5) + .max(50) + .required(), + AUTH_PERMISSION_TOKEN_EXPIRED: Joi.string() + .default('5m') + .required(), + + AUTH_JWT_PAYLOAD_ENCRYPT: Joi.boolean() + .default(false) + .required(), + AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_KEY: Joi.string() + .allow(null, '') + .min(20) + .max(50) + .optional(), + AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_IV: Joi.string() + .allow(null, '') + .min(16) + .max(50) + .optional(), + AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_KEY: Joi.string() + .allow(null, '') + .min(20) + .max(50) + .optional(), + AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_IV: Joi.string() + .allow(null, '') + .min(16) + .max(50) + .optional(), + AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_KEY: Joi.string() + .allow(null, '') + .min(20) + .max(50) + .optional(), + AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_IV: Joi.string() + .allow(null, '') + .min(16) + .max(50) + .optional(), + + AWS_CREDENTIAL_KEY: Joi.string().allow(null, '').optional(), + AWS_CREDENTIAL_SECRET: Joi.string().allow(null, '').optional(), + AWS_S3_REGION: Joi.string().allow(null, '').optional(), + AWS_S3_BUCKET: Joi.string().allow(null, '').optional(), + }), + validationOptions: { + allowUnknown: true, + abortEarly: true, + }, + }), + MongooseModule.forRootAsync({ + connectionName: DATABASE_CONNECTION_NAME, + imports: [DatabaseOptionsModule], + inject: [DatabaseOptionsService], + useFactory: (databaseOptionsService: DatabaseOptionsService) => + databaseOptionsService.createOptions(), + }), + MessageModule, + HelperModule, + PaginationModule, + ErrorModule, + DebuggerModule.forRoot(), + ResponseModule, + RequestModule, + SettingModule, + LoggerModule, + ApiKeyModule, + AuthModule, + ], +}) +export class CommonModule {} diff --git a/src/common/dashboard/constants/dashboard.doc.constant.ts b/src/common/dashboard/constants/dashboard.doc.constant.ts new file mode 100644 index 0000000..b0d4036 --- /dev/null +++ b/src/common/dashboard/constants/dashboard.doc.constant.ts @@ -0,0 +1,21 @@ +import { faker } from '@faker-js/faker'; + +export const DashboardDocQueryStartDate = [ + { + name: 'startDate', + allowEmptyValue: true, + required: false, + type: 'string', + example: faker.date.recent().toString(), + }, +]; + +export const DashboardDocQueryEndDate = [ + { + name: 'endDate', + allowEmptyValue: true, + required: false, + type: 'string', + example: faker.date.recent().toString(), + }, +]; diff --git a/src/common/dashboard/dashboard.module.ts b/src/common/dashboard/dashboard.module.ts new file mode 100644 index 0000000..1b2ebfe --- /dev/null +++ b/src/common/dashboard/dashboard.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { DashboardService } from 'src/common/dashboard/services/dashboard.service'; + +@Module({ + controllers: [], + providers: [DashboardService], + exports: [DashboardService], + imports: [], +}) +export class DashboardModule {} diff --git a/src/common/dashboard/dtos/dashboard.ts b/src/common/dashboard/dtos/dashboard.ts new file mode 100644 index 0000000..49b4a7a --- /dev/null +++ b/src/common/dashboard/dtos/dashboard.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsDate, IsOptional, ValidateIf } from 'class-validator'; +import { MinGreaterThan } from 'src/common/request/validations/request.min-greater-than.validation'; + +export class DashboardDto { + @ApiProperty({ + name: 'startDate', + required: false, + nullable: true, + }) + @IsDate() + @IsOptional() + @Type(() => Date) + @ValidateIf((e) => e.startDate !== '' || e.endDate !== '') + startDate?: Date; + + @ApiProperty({ + name: 'endDate', + required: false, + nullable: true, + }) + @IsDate() + @IsOptional() + @MinGreaterThan('startDate') + @Type(() => Date) + @ValidateIf((e) => e.startDate !== '' || e.endDate !== '') + endDate?: Date; +} diff --git a/src/common/dashboard/interfaces/dashboard.interface.ts b/src/common/dashboard/interfaces/dashboard.interface.ts new file mode 100644 index 0000000..f206b7a --- /dev/null +++ b/src/common/dashboard/interfaces/dashboard.interface.ts @@ -0,0 +1,18 @@ +export interface IDashboardStartAndEndDate { + startDate: Date; + endDate: Date; +} + +export interface IDashboardStartAndEndYear { + startYear: number; + endYear: number; +} + +export interface IDashboardStartAndEnd { + month: number; + year: number; +} + +export interface IDashboardMonthAndYear extends Partial { + total: number; +} diff --git a/src/common/dashboard/interfaces/dashboard.service.interface.ts b/src/common/dashboard/interfaces/dashboard.service.interface.ts new file mode 100644 index 0000000..272cd0d --- /dev/null +++ b/src/common/dashboard/interfaces/dashboard.service.interface.ts @@ -0,0 +1,24 @@ +import { DashboardDto } from 'src/common/dashboard/dtos/dashboard'; +import { + IDashboardStartAndEnd, + IDashboardStartAndEndDate, + IDashboardStartAndEndYear, +} from 'src/common/dashboard/interfaces/dashboard.interface'; + +export interface IDashboardService { + getStartAndEndDate(date: DashboardDto): Promise; + + getMonths(): Promise; + + getStartAndEndYear({ + startDate, + endDate, + }: IDashboardStartAndEndDate): Promise; + + getStartAndEndMonth({ + month, + year, + }: IDashboardStartAndEnd): Promise; + + getPercentage(value: number, total: number): Promise; +} diff --git a/src/common/dashboard/serializations/dashboard.month-and-year.serialization.ts b/src/common/dashboard/serializations/dashboard.month-and-year.serialization.ts new file mode 100644 index 0000000..7bb0cc9 --- /dev/null +++ b/src/common/dashboard/serializations/dashboard.month-and-year.serialization.ts @@ -0,0 +1,38 @@ +import { ApiProperty, OmitType } from '@nestjs/swagger'; + +export class DashboardMonthAndYearSerialization { + @ApiProperty({ + name: 'total', + example: 12, + }) + month: number; + + @ApiProperty({ + name: 'total', + example: 2001, + }) + year: number; + + @ApiProperty({ + name: 'total', + description: 'total of target', + required: true, + nullable: false, + example: 0, + }) + total: number; +} + +export class DashboardMonthAndYearPercentageSerialization extends OmitType( + DashboardMonthAndYearSerialization, + ['total'] as const +) { + @ApiProperty({ + name: 'percent', + description: 'Percent of target', + required: true, + nullable: false, + example: 15.4, + }) + percent: number; +} diff --git a/src/common/dashboard/serializations/dashboard.serialization.ts b/src/common/dashboard/serializations/dashboard.serialization.ts new file mode 100644 index 0000000..c92f0e0 --- /dev/null +++ b/src/common/dashboard/serializations/dashboard.serialization.ts @@ -0,0 +1,12 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; + +export class DashboardSerialization { + @ApiProperty({ + name: 'total', + example: faker.random.numeric(4, { allowLeadingZeros: false }), + description: 'Total user', + nullable: false, + }) + total: number; +} diff --git a/src/common/dashboard/services/dashboard.service.ts b/src/common/dashboard/services/dashboard.service.ts new file mode 100644 index 0000000..2d14d9f --- /dev/null +++ b/src/common/dashboard/services/dashboard.service.ts @@ -0,0 +1,81 @@ +import { DashboardDto } from 'src/common/dashboard/dtos/dashboard'; +import { + IDashboardStartAndEnd, + IDashboardStartAndEndDate, + IDashboardStartAndEndYear, +} from 'src/common/dashboard/interfaces/dashboard.interface'; +import { IDashboardService } from 'src/common/dashboard/interfaces/dashboard.service.interface'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; + +export class DashboardService implements IDashboardService { + constructor( + private readonly helperDateService: HelperDateService, + private readonly helperNumberService: HelperNumberService + ) {} + + async getStartAndEndDate( + date: DashboardDto + ): Promise { + const today = this.helperDateService.create(); + let { startDate, endDate } = date; + + if (!startDate && !endDate) { + startDate = this.helperDateService.startOfYear(today); + endDate = this.helperDateService.endOfYear(today); + } else { + if (!startDate) { + startDate = this.helperDateService.startOfDay(); + } else { + startDate = this.helperDateService.startOfDay(startDate); + } + + if (!endDate) { + endDate = this.helperDateService.endOfDay(); + } else { + endDate = this.helperDateService.endOfDay(endDate); + } + } + + return { + startDate, + endDate, + }; + } + + async getMonths(): Promise { + return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + } + + async getStartAndEndYear({ + startDate, + endDate, + }: IDashboardStartAndEndDate): Promise { + return { + startYear: startDate.getFullYear(), + endYear: endDate.getFullYear(), + }; + } + + async getStartAndEndMonth({ + month, + year, + }: IDashboardStartAndEnd): Promise { + const monthString = `${month}`.padStart(2, '0'); + const date: Date = this.helperDateService.create( + `${year}-${monthString}-01` + ); + + const startDate = this.helperDateService.startOfMonth(date); + const endDate = this.helperDateService.endOfMonth(date); + + return { + startDate, + endDate, + }; + } + + async getPercentage(value: number, total: number): Promise { + return this.helperNumberService.percent(value, total); + } +} diff --git a/src/common/database/abstracts/database.base-entity.abstract.ts b/src/common/database/abstracts/database.base-entity.abstract.ts new file mode 100644 index 0000000..31c0b66 --- /dev/null +++ b/src/common/database/abstracts/database.base-entity.abstract.ts @@ -0,0 +1,12 @@ +import { + DATABASE_CREATED_AT_FIELD_NAME, + DATABASE_DELETED_AT_FIELD_NAME, + DATABASE_UPDATED_AT_FIELD_NAME, +} from 'src/common/database/constants/database.constant'; + +export abstract class DatabaseBaseEntityAbstract { + abstract _id: T; + abstract [DATABASE_DELETED_AT_FIELD_NAME]?: Date; + abstract [DATABASE_CREATED_AT_FIELD_NAME]?: Date; + abstract [DATABASE_UPDATED_AT_FIELD_NAME]?: Date; +} diff --git a/src/common/database/abstracts/database.base-repository.abstract.ts b/src/common/database/abstracts/database.base-repository.abstract.ts new file mode 100644 index 0000000..5739585 --- /dev/null +++ b/src/common/database/abstracts/database.base-repository.abstract.ts @@ -0,0 +1,116 @@ +import { + IDatabaseCreateOptions, + IDatabaseExistOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseCreateManyOptions, + IDatabaseManyOptions, + IDatabaseSoftDeleteManyOptions, + IDatabaseRestoreManyOptions, + IDatabaseRawOptions, +} from 'src/common/database/interfaces/database.interface'; + +export abstract class DatabaseBaseRepositoryAbstract { + abstract findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + + abstract findAllDistinct( + fieldDistinct: string, + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + + abstract findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise; + + abstract findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise; + + abstract findOneAndLock( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise; + + abstract findOneByIdAndLock( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise; + + abstract getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise; + + abstract exists( + find: Record, + options?: IDatabaseExistOptions + ): Promise; + + abstract create( + data: Dto, + options?: IDatabaseCreateOptions + ): Promise; + + abstract save(repository: Entity): Promise; + + abstract delete(repository: Entity): Promise; + + abstract softDelete(repository: Entity): Promise; + + abstract restore(repository: Entity): Promise; + + abstract createMany( + data: Dto[], + options?: IDatabaseCreateManyOptions + ): Promise; + + abstract deleteManyByIds( + _id: string[], + options?: IDatabaseManyOptions + ): Promise; + + abstract deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise; + + abstract softDeleteManyByIds( + _id: string[], + options?: IDatabaseSoftDeleteManyOptions + ): Promise; + + abstract softDeleteMany( + find: Record, + options?: IDatabaseSoftDeleteManyOptions + ): Promise; + + abstract restoreManyByIds( + _id: string[], + options?: IDatabaseRestoreManyOptions + ): Promise; + + abstract restoreMany( + find: Record, + options?: IDatabaseRestoreManyOptions + ): Promise; + + abstract updateMany( + find: Record, + data: Dto, + options?: IDatabaseManyOptions + ): Promise; + + abstract raw( + rawOperation: RawQuery, + options?: IDatabaseRawOptions + ): Promise; + + abstract model(): Promise; +} diff --git a/src/common/database/abstracts/mongo/entities/database.mongo.object-id.entity.abstract.ts b/src/common/database/abstracts/mongo/entities/database.mongo.object-id.entity.abstract.ts new file mode 100644 index 0000000..294116a --- /dev/null +++ b/src/common/database/abstracts/mongo/entities/database.mongo.object-id.entity.abstract.ts @@ -0,0 +1,38 @@ +import { Prop } from '@nestjs/mongoose'; +import { Types } from 'mongoose'; +import { DatabaseBaseEntityAbstract } from 'src/common/database/abstracts/database.base-entity.abstract'; +import { + DATABASE_CREATED_AT_FIELD_NAME, + DATABASE_DELETED_AT_FIELD_NAME, + DATABASE_UPDATED_AT_FIELD_NAME, +} from 'src/common/database/constants/database.constant'; +import { DatabaseDefaultObjectId } from 'src/common/database/constants/database.function.constant'; + +export abstract class DatabaseMongoObjectIdEntityAbstract extends DatabaseBaseEntityAbstract { + @Prop({ + type: Types.ObjectId, + default: DatabaseDefaultObjectId, + }) + _id: Types.ObjectId; + + @Prop({ + required: false, + index: true, + type: Date, + }) + [DATABASE_DELETED_AT_FIELD_NAME]?: Date; + + @Prop({ + required: false, + index: 'asc', + type: Date, + }) + [DATABASE_CREATED_AT_FIELD_NAME]?: Date; + + @Prop({ + required: false, + index: 'desc', + type: Date, + }) + [DATABASE_UPDATED_AT_FIELD_NAME]?: Date; +} diff --git a/src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract.ts b/src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract.ts new file mode 100644 index 0000000..7acdb5f --- /dev/null +++ b/src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract.ts @@ -0,0 +1,37 @@ +import { Prop } from '@nestjs/mongoose'; +import { DatabaseBaseEntityAbstract } from 'src/common/database/abstracts/database.base-entity.abstract'; +import { + DATABASE_CREATED_AT_FIELD_NAME, + DATABASE_DELETED_AT_FIELD_NAME, + DATABASE_UPDATED_AT_FIELD_NAME, +} from 'src/common/database/constants/database.constant'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; + +export abstract class DatabaseMongoUUIDEntityAbstract extends DatabaseBaseEntityAbstract { + @Prop({ + type: String, + default: DatabaseDefaultUUID, + }) + _id: string; + + @Prop({ + required: false, + index: true, + type: Date, + }) + [DATABASE_DELETED_AT_FIELD_NAME]?: Date; + + @Prop({ + required: false, + index: 'asc', + type: Date, + }) + [DATABASE_CREATED_AT_FIELD_NAME]?: Date; + + @Prop({ + required: false, + index: 'desc', + type: Date, + }) + [DATABASE_UPDATED_AT_FIELD_NAME]?: Date; +} diff --git a/src/common/database/abstracts/mongo/repositories/database.mongo.object-id.repository.abstract.ts b/src/common/database/abstracts/mongo/repositories/database.mongo.object-id.repository.abstract.ts new file mode 100644 index 0000000..2e824e5 --- /dev/null +++ b/src/common/database/abstracts/mongo/repositories/database.mongo.object-id.repository.abstract.ts @@ -0,0 +1,746 @@ +import { + ClientSession, + Model, + PipelineStage, + PopulateOptions, + Types, + Document, +} from 'mongoose'; +import { DatabaseBaseRepositoryAbstract } from 'src/common/database/abstracts/database.base-repository.abstract'; +import { DATABASE_DELETED_AT_FIELD_NAME } from 'src/common/database/constants/database.constant'; +import { + IDatabaseCreateOptions, + IDatabaseExistOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseCreateManyOptions, + IDatabaseManyOptions, + IDatabaseSoftDeleteManyOptions, + IDatabaseRestoreManyOptions, + IDatabaseRawOptions, +} from 'src/common/database/interfaces/database.interface'; + +export abstract class DatabaseMongoObjectIdRepositoryAbstract< + Entity, + EntityDocument +> extends DatabaseBaseRepositoryAbstract { + protected _repository: Model; + protected _joinOnFind?: PopulateOptions | PopulateOptions[]; + + constructor( + repository: Model, + options?: PopulateOptions | PopulateOptions[] + ) { + super(); + + this._repository = repository; + this._joinOnFind = options; + } + + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + const findAll = this._repository.find(find); + + if (options?.withDeleted) { + findAll.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + findAll.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.select) { + findAll.select(options.select); + } + + if (options?.paging) { + findAll.limit(options.paging.limit).skip(options.paging.offset); + } + + if (options?.order) { + findAll.sort(options.order); + } + + if (options?.join) { + findAll.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findAll.session(options.session); + } + + return findAll.lean() as any; + } + + async findAllDistinct( + fieldDistinct: string, + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + const findAll = this._repository.distinct( + fieldDistinct, + find + ); + + if (options?.withDeleted) { + findAll.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + findAll.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.select) { + findAll.select(options.select); + } + + if (options?.paging) { + findAll.limit(options.paging.limit).skip(options.paging.offset); + } + + if (options?.order) { + findAll.sort(options.order); + } + + if (options?.join) { + findAll.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findAll.session(options.session); + } + + return findAll.lean() as any; + } + + async findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findOne(find); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.select) { + findOne.select(options.select); + } + + if (options?.join) { + findOne.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + return findOne.exec() as any; + } + + async findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findById( + new Types.ObjectId(_id) + ); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.select) { + findOne.select(options.select); + } + + if (options?.join) { + findOne.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + return findOne.exec() as any; + } + + async findOneAndLock( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findOneAndUpdate( + find, + { + new: true, + useFindAndModify: false, + } + ); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.select) { + findOne.select(options.select); + } + + if (options?.join) { + findOne.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + return findOne.exec() as T; + } + + async findOneByIdAndLock( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findByIdAndUpdate( + new Types.ObjectId(_id), + { + new: true, + useFindAndModify: false, + } + ); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.select) { + findOne.select(options.select); + } + + if (options?.join) { + findOne.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + return findOne.exec() as T; + } + + async getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise { + const count = this._repository.countDocuments(find); + + if (options?.withDeleted) { + count.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + count.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.session) { + count.session(options.session); + } + + if (options?.join) { + count.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + return count; + } + + async exists( + find: Record, + options?: IDatabaseExistOptions + ): Promise { + + if (options?.excludeId) { + find= { + ...find, + _id: { + $nin: options?.excludeId.map( + (val) => new Types.ObjectId(val) + ) ?? [], + } + } + } + + const exist = this._repository.exists(find); + + if (options?.withDeleted) { + exist.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + exist.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.session) { + exist.session(options.session); + } + + if (options?.join) { + exist.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + const result = await exist; + return result ? true : false; + } + + async create( + data: Dto, + options?: IDatabaseCreateOptions + ): Promise { + const dataCreate: Record = data; + dataCreate._id = new Types.ObjectId(options?._id); + + const created = await this._repository.create([dataCreate], { + session: options ? options.session : undefined, + }); + + return created[0] as EntityDocument; + } + + async save( + repository: EntityDocument & Document + ): Promise { + return repository.save(); + } + + async delete( + repository: EntityDocument & Document + ): Promise { + return repository.deleteOne(); + } + + async softDelete( + repository: EntityDocument & + Document & { deletedAt?: Date } + ): Promise { + repository.deletedAt = new Date(); + return repository.save(); + } + + async restore( + repository: EntityDocument & + Document & { deletedAt?: Date } + ): Promise { + repository.deletedAt = undefined; + return repository.save(); + } + + // bulk + async createMany( + data: Dto[], + options?: IDatabaseCreateManyOptions + ): Promise { + const dataCreate: Record[] = data.map( + (val: Record) => ({ + ...val, + _id: new Types.ObjectId(val._id), + }) + ); + + const create = this._repository.insertMany(dataCreate, { + session: options ? options.session : undefined, + }); + + try { + await create; + return true; + } catch (err: unknown) { + throw err; + } + } + + async deleteManyByIds( + _id: string[], + options?: IDatabaseManyOptions + ): Promise { + const del = this._repository.deleteMany({ + _id: { + $in: _id.map((val) => new Types.ObjectId(val)), + }, + }); + + if (options?.session) { + del.session(options.session); + } + + if (options?.join) { + del.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await del; + return true; + } catch (err: unknown) { + throw err; + } + } + + async deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise { + const del = this._repository.deleteMany(find); + + if (options?.session) { + del.session(options.session); + } + + if (options?.join) { + del.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await del; + return true; + } catch (err: unknown) { + throw err; + } + } + + async softDeleteManyByIds( + _id: string[], + options?: IDatabaseSoftDeleteManyOptions + ): Promise { + const softDel = this._repository + .updateMany( + { + _id: { + $in: _id.map((val) => new Types.ObjectId(val)), + }, + }, + { + $set: { + deletedAt: new Date(), + }, + } + ) + .where(DATABASE_DELETED_AT_FIELD_NAME) + .exists(false); + + if (options?.session) { + softDel.session(options.session); + } + + if (options?.join) { + softDel.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await softDel; + return true; + } catch (err: unknown) { + throw err; + } + } + + async softDeleteMany( + find: Record, + options?: IDatabaseSoftDeleteManyOptions + ): Promise { + const softDel = this._repository + .updateMany(find, { + $set: { + deletedAt: new Date(), + }, + }) + .where(DATABASE_DELETED_AT_FIELD_NAME) + .exists(false); + + if (options?.session) { + softDel.session(options.session); + } + + if (options?.join) { + softDel.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await softDel; + return true; + } catch (err: unknown) { + throw err; + } + } + + async restoreManyByIds( + _id: string[], + options?: IDatabaseRestoreManyOptions + ): Promise { + const rest = this._repository + .updateMany( + { + _id: { + $in: _id.map((val) => new Types.ObjectId(val)), + }, + }, + { + $set: { + deletedAt: undefined, + }, + } + ) + .where(DATABASE_DELETED_AT_FIELD_NAME) + .exists(true); + + if (options?.session) { + rest.session(options.session); + } + + if (options?.join) { + rest.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await rest; + return true; + } catch (err: unknown) { + throw err; + } + } + + async restoreMany( + find: Record, + options?: IDatabaseRestoreManyOptions + ): Promise { + const rest = this._repository + .updateMany(find, { + $set: { + deletedAt: undefined, + }, + }) + .where(DATABASE_DELETED_AT_FIELD_NAME) + .exists(true); + + if (options?.session) { + rest.session(options.session); + } + + if (options?.join) { + rest.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await rest; + return true; + } catch (err: unknown) { + throw err; + } + } + + async updateMany( + find: Record, + data: Dto, + options?: IDatabaseManyOptions + ): Promise { + const update = this._repository + .updateMany(find, { + $set: data, + }) + .where(DATABASE_DELETED_AT_FIELD_NAME) + .exists(false); + + if (options?.session) { + update.session(options.session as ClientSession); + } + + if (options?.join) { + update.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await update; + return true; + } catch (err: unknown) { + throw err; + } + } + + // raw + async raw( + rawOperation: RawQuery, + options?: IDatabaseRawOptions + ): Promise { + if (!Array.isArray(rawOperation)) { + throw new Error('Must in array'); + } + + const pipeline: PipelineStage[] = rawOperation; + + if (options?.withDeleted) { + pipeline.push({ + $match: { + $or: [ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { + $exists: false, + }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ], + }, + }); + } else { + pipeline.push({ + $match: { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + }); + } + + const aggregate = this._repository.aggregate(pipeline); + + if (options?.session) { + aggregate.session(options?.session); + } + + return aggregate; + } + + async model(): Promise> { + return this._repository; + } +} diff --git a/src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract.ts b/src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract.ts new file mode 100644 index 0000000..5a253a5 --- /dev/null +++ b/src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract.ts @@ -0,0 +1,734 @@ +import { + ClientSession, + Model, + PipelineStage, + PopulateOptions, + Document, +} from 'mongoose'; +import { DatabaseBaseRepositoryAbstract } from 'src/common/database/abstracts/database.base-repository.abstract'; +import { DATABASE_DELETED_AT_FIELD_NAME } from 'src/common/database/constants/database.constant'; +import { + IDatabaseCreateOptions, + IDatabaseExistOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseCreateManyOptions, + IDatabaseManyOptions, + IDatabaseSoftDeleteManyOptions, + IDatabaseRestoreManyOptions, + IDatabaseRawOptions, +} from 'src/common/database/interfaces/database.interface'; + +export abstract class DatabaseMongoUUIDRepositoryAbstract< + Entity, + EntityDocument +> extends DatabaseBaseRepositoryAbstract { + protected _repository: Model; + protected _joinOnFind?: PopulateOptions | PopulateOptions[]; + + constructor( + repository: Model, + options?: PopulateOptions | PopulateOptions[] + ) { + super(); + + this._repository = repository; + this._joinOnFind = options; + } + + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + const findAll = this._repository.find(find); + + if (options?.withDeleted) { + findAll.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + findAll.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.select) { + findAll.select(options.select); + } + + if (options?.paging) { + findAll.limit(options.paging.limit).skip(options.paging.offset); + } + + if (options?.order) { + findAll.sort(options.order); + } + + if (options?.join) { + findAll.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findAll.session(options.session); + } + + + return findAll.lean() as any; + } + + async findAllDistinct( + fieldDistinct: string, + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + const findAll = this._repository.distinct( + fieldDistinct, + find + ); + + if (options?.withDeleted) { + findAll.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + findAll.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.select) { + findAll.select(options.select); + } + + if (options?.paging) { + findAll.limit(options.paging.limit).skip(options.paging.offset); + } + + if (options?.order) { + findAll.sort(options.order); + } + + if (options?.join) { + findAll.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findAll.session(options.session); + } + + return findAll.lean() as any; + } + async findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findOne(find); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.select) { + findOne.select(options.select); + } + + if (options?.join) { + findOne.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + return findOne.exec() as any; + } + + async findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findById(_id); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.select) { + findOne.select(options.select); + } + + if (options?.join) { + findOne.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + + return findOne.exec() as any; + } + + async findOneAndLock( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findOneAndUpdate( + find, + { + new: true, + useFindAndModify: false, + } + ); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.select) { + findOne.select(options.select); + } + + if (options?.join) { + findOne.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + return findOne.exec() as any; + } + + async findOneByIdAndLock( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + const findOne = this._repository.findByIdAndUpdate( + _id, + { + new: true, + useFindAndModify: false, + } + ); + + if (options?.withDeleted) { + findOne.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.select) { + findOne.select(options.select); + } + + if (options?.join) { + findOne.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + if (options?.session) { + findOne.session(options.session); + } + + if (options?.order) { + findOne.sort(options.order); + } + + return findOne.exec() as any; + } + + async getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise { + const count = this._repository.countDocuments(find); + + if (options?.withDeleted) { + count.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + count.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.session) { + count.session(options.session); + } + + if (options?.join) { + count.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + return count; + } + + async exists( + find: Record, + options?: IDatabaseExistOptions + ): Promise { + + if (options?.excludeId) { + find = { + ...find, + _id: { + $nin: options?.excludeId ?? [], + }, + }; + } + + const exist = this._repository.exists(find); + + if (options?.withDeleted) { + exist.or([ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ]); + } else { + exist.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false); + } + + if (options?.session) { + exist.session(options.session); + } + + if (options?.join) { + exist.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + const result = await exist; + return result ? true : false; + } + + async create( + data: Dto, + options?: IDatabaseCreateOptions + ): Promise { + const dataCreate: Record = data; + if (options?._id) { + dataCreate._id = options._id; + } + + const created = await this._repository.create([dataCreate], { + session: options ? options.session : undefined, + }); + + return created[0] as EntityDocument; + } + + async save( + repository: EntityDocument & Document + ): Promise { + return repository.save(); + } + + async delete( + repository: EntityDocument & Document + ): Promise { + return repository.deleteOne(); + } + + async softDelete( + repository: EntityDocument & Document & { deletedAt?: Date } + ): Promise { + repository.deletedAt = new Date(); + return repository.save(); + } + + async restore( + repository: EntityDocument & Document & { deletedAt?: Date } + ): Promise { + repository.deletedAt = undefined; + return repository.save(); + } + + // bulk + async createMany( + data: Dto[], + options?: IDatabaseCreateManyOptions + ): Promise { + const create = this._repository.insertMany(data, { + session: options ? options.session : undefined, + }); + + try { + await create; + return true; + } catch (err: unknown) { + throw err; + } + } + + async deleteManyByIds( + _id: string[], + options?: IDatabaseManyOptions + ): Promise { + const del = this._repository.deleteMany({ + _id: { + $in: _id, + }, + }); + + if (options?.session) { + del.session(options.session); + } + + if (options?.join) { + del.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await del; + return true; + } catch (err: unknown) { + throw err; + } + } + + async deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise { + const del = this._repository.deleteMany(find); + + if (options?.session) { + del.session(options.session); + } + + if (options?.join) { + del.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await del; + return true; + } catch (err: unknown) { + throw err; + } + } + + async softDeleteManyByIds( + _id: string[], + options?: IDatabaseSoftDeleteManyOptions + ): Promise { + const softDel = this._repository + .updateMany( + { + _id: { + $in: _id, + }, + }, + { + $set: { + deletedAt: new Date(), + }, + } + ) + .where(DATABASE_DELETED_AT_FIELD_NAME) + .exists(false); + + if (options?.session) { + softDel.session(options.session); + } + + if (options?.join) { + softDel.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await softDel; + return true; + } catch (err: unknown) { + throw err; + } + } + + async softDeleteMany( + find: Record, + options?: IDatabaseSoftDeleteManyOptions + ): Promise { + const softDel = this._repository + .updateMany(find, { + $set: { + deletedAt: new Date(), + }, + }) + .where(DATABASE_DELETED_AT_FIELD_NAME) + .exists(false); + + if (options?.session) { + softDel.session(options.session); + } + + if (options?.join) { + softDel.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await softDel; + return true; + } catch (err: unknown) { + throw err; + } + } + + async restoreManyByIds( + _id: string[], + options?: IDatabaseRestoreManyOptions + ): Promise { + const rest = this._repository + .updateMany( + { + _id: { + $in: _id, + }, + }, + { + $set: { + deletedAt: undefined, + }, + } + ) + .where(DATABASE_DELETED_AT_FIELD_NAME) + .exists(true); + + if (options?.session) { + rest.session(options.session); + } + + if (options?.join) { + rest.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await rest; + return true; + } catch (err: unknown) { + throw err; + } + } + + async restoreMany( + find: Record, + options?: IDatabaseRestoreManyOptions + ): Promise { + const rest = this._repository + .updateMany(find, { + $set: { + deletedAt: undefined, + }, + }) + .where(DATABASE_DELETED_AT_FIELD_NAME) + .exists(true); + + if (options?.session) { + rest.session(options.session); + } + + if (options?.join) { + rest.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await rest; + return true; + } catch (err: unknown) { + throw err; + } + } + + async updateMany( + find: Record, + data: Dto, + options?: IDatabaseManyOptions + ): Promise { + const update = this._repository + .updateMany(find, { + $set: data, + }) + .where(DATABASE_DELETED_AT_FIELD_NAME) + .exists(false); + + if (options?.session) { + update.session(options.session as ClientSession); + } + + if (options?.join) { + update.populate( + typeof options.join === 'boolean' + ? this._joinOnFind + : (options.join as PopulateOptions | PopulateOptions[]) + ); + } + + try { + await update; + return true; + } catch (err: unknown) { + throw err; + } + } + + async raw( + rawOperation: RawQuery, + options?: IDatabaseRawOptions + ): Promise { + if (!Array.isArray(rawOperation)) { + throw new Error('Must in array'); + } + + const pipeline: PipelineStage[] = rawOperation; + + if (options?.withDeleted) { + pipeline.push({ + $match: { + $or: [ + { + [DATABASE_DELETED_AT_FIELD_NAME]: { + $exists: false, + }, + }, + { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true }, + }, + ], + }, + }); + } else { + pipeline.push({ + $match: { + [DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false }, + }, + }); + } + + const aggregate = this._repository.aggregate(pipeline); + + if (options?.session) { + aggregate.session(options?.session); + } + + return aggregate; + } + + async model(): Promise> { + return this._repository; + } +} diff --git a/src/common/database/constants/database.constant.ts b/src/common/database/constants/database.constant.ts new file mode 100644 index 0000000..3a992e2 --- /dev/null +++ b/src/common/database/constants/database.constant.ts @@ -0,0 +1,5 @@ +export const DATABASE_CONNECTION_NAME = 'PrimaryConnectionDatabase'; + +export const DATABASE_DELETED_AT_FIELD_NAME = 'deletedAt'; +export const DATABASE_UPDATED_AT_FIELD_NAME = 'updatedAt'; +export const DATABASE_CREATED_AT_FIELD_NAME = 'createdAt'; diff --git a/src/common/database/constants/database.function.constant.ts b/src/common/database/constants/database.function.constant.ts new file mode 100644 index 0000000..3dcaaa9 --- /dev/null +++ b/src/common/database/constants/database.function.constant.ts @@ -0,0 +1,6 @@ +import { Types } from 'mongoose'; +import { v4 as uuidV4 } from 'uuid'; + +export const DatabaseDefaultUUID = uuidV4; + +export const DatabaseDefaultObjectId = () => new Types.ObjectId(); diff --git a/src/common/database/database.options.module.ts b/src/common/database/database.options.module.ts new file mode 100644 index 0000000..2913972 --- /dev/null +++ b/src/common/database/database.options.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { DatabaseOptionsService } from 'src/common/database/services/database.options.service'; + +@Module({ + providers: [DatabaseOptionsService], + exports: [DatabaseOptionsService], + imports: [], + controllers: [], +}) +export class DatabaseOptionsModule {} diff --git a/src/common/database/decorators/database.decorator.ts b/src/common/database/decorators/database.decorator.ts new file mode 100644 index 0000000..3cfbc8a --- /dev/null +++ b/src/common/database/decorators/database.decorator.ts @@ -0,0 +1,35 @@ +import { + InjectConnection, + InjectModel, + Schema, + SchemaOptions, +} from '@nestjs/mongoose'; +import { + DATABASE_CONNECTION_NAME, + DATABASE_CREATED_AT_FIELD_NAME, + DATABASE_UPDATED_AT_FIELD_NAME, +} from 'src/common/database/constants/database.constant'; + +export function DatabaseConnection( + connectionName?: string +): ParameterDecorator { + return InjectConnection(connectionName ?? DATABASE_CONNECTION_NAME); +} + +export function DatabaseModel( + entity: any, + connectionName?: string +): ParameterDecorator { + return InjectModel(entity, connectionName ?? DATABASE_CONNECTION_NAME); +} + +export function DatabaseEntity(options?: SchemaOptions): ClassDecorator { + return Schema({ + ...options, + versionKey: false, + timestamps: { + createdAt: DATABASE_CREATED_AT_FIELD_NAME, + updatedAt: DATABASE_UPDATED_AT_FIELD_NAME, + }, + }); +} diff --git a/src/common/database/interfaces/database.interface.ts b/src/common/database/interfaces/database.interface.ts new file mode 100644 index 0000000..4b81bbf --- /dev/null +++ b/src/common/database/interfaces/database.interface.ts @@ -0,0 +1,54 @@ +import { PopulateOptions } from 'mongoose'; +import { IPaginationOptions } from 'src/common/pagination/interfaces/pagination.interface'; + +// find one +export interface IDatabaseFindOneOptions + extends Pick { + select?: Record; + join?: boolean | PopulateOptions | PopulateOptions[]; + session?: T; + withDeleted?: boolean; +} + +export type IDatabaseOptions = Pick< + IDatabaseFindOneOptions, + 'session' | 'withDeleted' | 'join' +>; + +// find +export interface IDatabaseFindAllOptions + extends IPaginationOptions, + Omit, 'order'> {} + +// create + +export interface IDatabaseCreateOptions + extends Pick, 'session'> { + _id?: string; +} + +// exist + +export interface IDatabaseExistOptions extends IDatabaseOptions { + excludeId?: string[]; +} + +// bulk +export type IDatabaseManyOptions = Pick< + IDatabaseFindOneOptions, + 'session' | 'join' +>; + +export type IDatabaseCreateManyOptions = Pick< + IDatabaseOptions, + 'session' +>; + +export type IDatabaseSoftDeleteManyOptions = IDatabaseManyOptions; + +export type IDatabaseRestoreManyOptions = IDatabaseManyOptions; + +export type IDatabaseRawOptions = Pick< + IDatabaseOptions, + 'session' | 'withDeleted' +>; diff --git a/src/common/database/interfaces/database.options-service.interface.ts b/src/common/database/interfaces/database.options-service.interface.ts new file mode 100644 index 0000000..e217fa3 --- /dev/null +++ b/src/common/database/interfaces/database.options-service.interface.ts @@ -0,0 +1,5 @@ +import { MongooseModuleOptions } from '@nestjs/mongoose'; + +export interface IDatabaseOptionsService { + createOptions(): MongooseModuleOptions; +} diff --git a/src/common/database/services/database.options.service.ts b/src/common/database/services/database.options.service.ts new file mode 100644 index 0000000..8765db4 --- /dev/null +++ b/src/common/database/services/database.options.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@nestjs/common'; +import { MongooseModuleOptions } from '@nestjs/mongoose'; +import mongoose from 'mongoose'; +import { ConfigService } from '@nestjs/config'; +import { IDatabaseOptionsService } from 'src/common/database/interfaces/database.options-service.interface'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant'; + +@Injectable() +export class DatabaseOptionsService implements IDatabaseOptionsService { + private readonly host: string; + private readonly database: string; + private readonly user: string; + private readonly password: string; + private readonly debug: boolean; + private readonly options: string; + private readonly env: string; + + constructor(private readonly configService: ConfigService) { + this.env = this.configService.get('app.env'); + this.host = this.configService.get('database.host'); + this.database = this.configService.get('database.name'); + this.user = this.configService.get('database.user'); + this.password = this.configService.get('database.password'); + this.debug = this.configService.get('database.debug'); + + this.options = this.configService.get('database.options') + ? `?${this.configService.get('database.options')}` + : ''; + } + + createOptions(): MongooseModuleOptions { + let uri = `${this.host}`; + + if (this.database) { + uri = `${uri}/${this.database}${this.options}`; + } + + if (this.env !== ENUM_APP_ENVIRONMENT.PRODUCTION) { + mongoose.set('debug', this.debug); + } + + const mongooseOptions: MongooseModuleOptions = { + uri, + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + autoCreate: true, + // useMongoClient: true, + }; + + if (this.user && this.password) { + mongooseOptions.auth = { + username: this.user, + password: this.password, + }; + } + + return mongooseOptions; + } +} diff --git a/src/common/debugger/constants/debugger.constant.ts b/src/common/debugger/constants/debugger.constant.ts new file mode 100644 index 0000000..a5bc018 --- /dev/null +++ b/src/common/debugger/constants/debugger.constant.ts @@ -0,0 +1,5 @@ +export const DEBUGGER_NAME = 'system'; + +export const DEBUGGER_HTTP_FORMAT = + "':remote-addr' - ':remote-user' - '[:date[iso]]' - 'HTTP/:http-version' - '[:status]' - ':method' - ':url' - 'Request Header :: :req-headers' - 'Request Params :: :req-params' - 'Request Body :: :req-body' - 'Response Header :: :res[header]' - 'Response Body :: :res-body' - ':response-time ms' - ':referrer' - ':user-agent'"; +export const DEBUGGER_HTTP_NAME = 'http'; diff --git a/src/common/debugger/debugger.module.ts b/src/common/debugger/debugger.module.ts new file mode 100644 index 0000000..89e242e --- /dev/null +++ b/src/common/debugger/debugger.module.ts @@ -0,0 +1,51 @@ +import { + DynamicModule, + ForwardReference, + Global, + Module, + Provider, + Type, +} from '@nestjs/common'; +import { WinstonModule } from 'nest-winston'; +import { DebuggerOptionsModule } from 'src/common/debugger/debugger.options.module'; +import { DebuggerMiddlewareModule } from 'src/common/debugger/middleware/debugger.middleware.module'; +import { DebuggerOptionService } from 'src/common/debugger/services/debugger.options.service'; +import { DebuggerService } from 'src/common/debugger/services/debugger.service'; + +@Global() +@Module({}) +export class DebuggerModule { + static forRoot(): DynamicModule { + const providers: Provider[] = []; + const imports: ( + | DynamicModule + | Type + | Promise + | ForwardReference + )[] = []; + + if ( + process.env.DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE === 'true' || + process.env.DEBUGGER_SYSTEM_WRITE_INTO_FILE === 'true' + ) { + providers.push(DebuggerService); + imports.push( + WinstonModule.forRootAsync({ + inject: [DebuggerOptionService], + imports: [DebuggerOptionsModule], + useFactory: ( + debuggerOptionsService: DebuggerOptionService + ) => debuggerOptionsService.createLogger(), + }) + ); + } + + return { + module: DebuggerModule, + providers, + exports: providers, + controllers: [], + imports: [...imports, DebuggerMiddlewareModule], + }; + } +} diff --git a/src/common/debugger/debugger.options.module.ts b/src/common/debugger/debugger.options.module.ts new file mode 100644 index 0000000..6e8c5f8 --- /dev/null +++ b/src/common/debugger/debugger.options.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { DebuggerOptionService } from 'src/common/debugger/services/debugger.options.service'; + +@Module({ + providers: [DebuggerOptionService], + exports: [DebuggerOptionService], + imports: [], +}) +export class DebuggerOptionsModule {} diff --git a/src/common/debugger/interfaces/debugger.interface.ts b/src/common/debugger/interfaces/debugger.interface.ts new file mode 100644 index 0000000..bb9cfcb --- /dev/null +++ b/src/common/debugger/interfaces/debugger.interface.ts @@ -0,0 +1,22 @@ +import { RotatingFileStream } from 'rotating-file-stream'; +import { Response } from 'express'; + +export interface IDebuggerLog { + description: string; + class?: string; + function?: string; + path?: string; +} + +export interface IDebuggerHttpConfigOptions { + readonly stream: RotatingFileStream; +} + +export interface IDebuggerHttpConfig { + readonly debuggerHttpFormat: string; + readonly debuggerHttpOptions?: IDebuggerHttpConfigOptions; +} + +export interface IDebuggerHttpMiddleware extends Response { + body: string; +} diff --git a/src/common/debugger/interfaces/debugger.options-service.interface.ts b/src/common/debugger/interfaces/debugger.options-service.interface.ts new file mode 100644 index 0000000..af8bb19 --- /dev/null +++ b/src/common/debugger/interfaces/debugger.options-service.interface.ts @@ -0,0 +1,5 @@ +import { LoggerOptions } from 'winston'; + +export interface IDebuggerOptionService { + createLogger(): LoggerOptions; +} diff --git a/src/common/debugger/interfaces/debugger.service.interface.ts b/src/common/debugger/interfaces/debugger.service.interface.ts new file mode 100644 index 0000000..509ff1f --- /dev/null +++ b/src/common/debugger/interfaces/debugger.service.interface.ts @@ -0,0 +1,11 @@ +import { IDebuggerLog } from 'src/common/debugger/interfaces/debugger.interface'; + +export interface IDebuggerService { + info(requestId: string, log: IDebuggerLog, data?: any): void; + + debug(requestId: string, log: IDebuggerLog, data?: any): void; + + warn(requestId: string, log: IDebuggerLog, data?: any): void; + + error(requestId: string, log: IDebuggerLog, data?: any): void; +} diff --git a/src/common/debugger/middleware/debugger.middleware.module.ts b/src/common/debugger/middleware/debugger.middleware.module.ts new file mode 100644 index 0000000..3ec2760 --- /dev/null +++ b/src/common/debugger/middleware/debugger.middleware.module.ts @@ -0,0 +1,21 @@ +import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; +import { + DebuggerHttpMiddleware, + DebuggerHttpResponseMiddleware, + DebuggerHttpWriteIntoConsoleMiddleware, + DebuggerHttpWriteIntoFileMiddleware, +} from 'src/common/debugger/middleware/http/debugger.http.middleware'; + +@Module({}) +export class DebuggerMiddlewareModule implements NestModule { + configure(consumer: MiddlewareConsumer): void { + consumer + .apply( + DebuggerHttpResponseMiddleware, + DebuggerHttpMiddleware, + DebuggerHttpWriteIntoConsoleMiddleware, + DebuggerHttpWriteIntoFileMiddleware + ) + .forRoutes('*'); + } +} diff --git a/src/common/debugger/middleware/http/debugger.http.middleware.ts b/src/common/debugger/middleware/http/debugger.http.middleware.ts new file mode 100644 index 0000000..5cbd51f --- /dev/null +++ b/src/common/debugger/middleware/http/debugger.http.middleware.ts @@ -0,0 +1,170 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import morgan from 'morgan'; +import { Request, Response, NextFunction } from 'express'; +import { createStream } from 'rotating-file-stream'; +import { ConfigService } from '@nestjs/config'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { + IDebuggerHttpConfig, + IDebuggerHttpConfigOptions, + IDebuggerHttpMiddleware, +} from 'src/common/debugger/interfaces/debugger.interface'; +import { + DEBUGGER_HTTP_FORMAT, + DEBUGGER_HTTP_NAME, +} from 'src/common/debugger/constants/debugger.constant'; + +@Injectable() +export class DebuggerHttpMiddleware implements NestMiddleware { + private readonly writeIntoFile: boolean; + private readonly writeIntoConsole: boolean; + + constructor(private readonly configService: ConfigService) { + this.writeIntoFile = this.configService.get( + 'debugger.http.writeIntoFile' + ); + this.writeIntoConsole = this.configService.get( + 'debugger.http.writeIntoConsole' + ); + } + + private customToken(): void { + morgan.token('req-params', (req: Request) => + JSON.stringify(req.params) + ); + + morgan.token('req-body', (req: Request) => JSON.stringify(req.body)); + + morgan.token( + 'res-body', + (req: Request, res: IDebuggerHttpMiddleware) => res.body + ); + + morgan.token('req-headers', (req: Request) => + JSON.stringify(req.headers) + ); + } + + async use(req: Request, res: Response, next: NextFunction): Promise { + if (this.writeIntoConsole || this.writeIntoFile) { + this.customToken(); + } + + next(); + } +} + +@Injectable() +export class DebuggerHttpWriteIntoFileMiddleware implements NestMiddleware { + private readonly writeIntoFile: boolean; + private readonly maxSize: string; + private readonly maxFiles: number; + + constructor( + private readonly configService: ConfigService, + private readonly helperDateService: HelperDateService + ) { + this.writeIntoFile = this.configService.get( + 'debugger.http.writeIntoFile' + ); + this.maxSize = this.configService.get('debugger.http.maxSize'); + this.maxFiles = this.configService.get( + 'debugger.http.maxFiles' + ); + } + + private async httpLogger(): Promise { + const date: string = this.helperDateService.format( + this.helperDateService.create() + ); + + const debuggerHttpOptions: IDebuggerHttpConfigOptions = { + stream: createStream(`${date}.log`, { + path: `./logs/${DEBUGGER_HTTP_NAME}/`, + maxSize: this.maxSize, + maxFiles: this.maxFiles, + compress: true, + interval: '1d', + }), + }; + + return { + debuggerHttpFormat: DEBUGGER_HTTP_FORMAT, + debuggerHttpOptions, + }; + } + + async use(req: Request, res: Response, next: NextFunction): Promise { + if (this.writeIntoFile) { + const config: IDebuggerHttpConfig = await this.httpLogger(); + + morgan(config.debuggerHttpFormat, config.debuggerHttpOptions)( + req, + res, + next + ); + } else { + next(); + } + } +} + +@Injectable() +export class DebuggerHttpWriteIntoConsoleMiddleware implements NestMiddleware { + private readonly writeIntoConsole: boolean; + + constructor(private readonly configService: ConfigService) { + this.writeIntoConsole = this.configService.get( + 'debugger.http.writeIntoConsole' + ); + } + + private async httpLogger(): Promise { + return { + debuggerHttpFormat: DEBUGGER_HTTP_FORMAT, + }; + } + + async use(req: Request, res: Response, next: NextFunction): Promise { + if (this.writeIntoConsole) { + const config: IDebuggerHttpConfig = await this.httpLogger(); + + morgan(config.debuggerHttpFormat)(req, res, next); + } else { + next(); + } + } +} + +@Injectable() +export class DebuggerHttpResponseMiddleware implements NestMiddleware { + private readonly writeIntoFile: boolean; + private readonly writeIntoConsole: boolean; + + constructor(private readonly configService: ConfigService) { + this.writeIntoConsole = this.configService.get( + 'debugger.http.writeIntoConsole' + ); + this.writeIntoFile = this.configService.get( + 'debugger.http.writeIntoFile' + ); + } + use(req: Request, res: Response, next: NextFunction): void { + if (this.writeIntoConsole || this.writeIntoFile) { + const send: any = res.send; + const resOld: any = res; + + // Add response data to request + // this is for morgan + resOld.send = (body: any) => { + resOld.body = body; + resOld.send = send; + resOld.send(body); + + res = resOld as Response; + }; + } + + next(); + } +} diff --git a/src/common/debugger/services/debugger.options.service.ts b/src/common/debugger/services/debugger.options.service.ts new file mode 100644 index 0000000..7bcc794 --- /dev/null +++ b/src/common/debugger/services/debugger.options.service.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { LoggerOptions } from 'winston'; +import winston from 'winston'; +import DailyRotateFile from 'winston-daily-rotate-file'; +import { DEBUGGER_NAME } from 'src/common/debugger/constants/debugger.constant'; +import { IDebuggerOptionService } from 'src/common/debugger/interfaces/debugger.options-service.interface'; + +@Injectable() +export class DebuggerOptionService implements IDebuggerOptionService { + private readonly writeIntoFile: boolean; + private readonly writeIntoConsole: boolean; + private readonly maxSize: string; + private readonly maxFiles: string; + + constructor(private configService: ConfigService) { + this.writeIntoFile = this.configService.get( + 'debugger.system.writeIntoFile' + ); + this.writeIntoConsole = this.configService.get( + 'debugger.system.writeIntoConsole' + ); + this.maxSize = this.configService.get( + 'debugger.system.maxSize' + ); + this.maxFiles = this.configService.get( + 'debugger.system.maxFiles' + ); + } + + createLogger(): LoggerOptions { + const transports = []; + + if (this.writeIntoFile) { + transports.push( + new DailyRotateFile({ + filename: `%DATE%.log`, + dirname: `logs/${DEBUGGER_NAME}/error`, + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: this.maxSize, + maxFiles: this.maxFiles, + level: 'error', + }) + ); + transports.push( + new DailyRotateFile({ + filename: `%DATE%.log`, + dirname: `logs/${DEBUGGER_NAME}/default`, + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: this.maxSize, + maxFiles: this.maxFiles, + level: 'info', + }) + ); + transports.push( + new DailyRotateFile({ + filename: `%DATE%.log`, + dirname: `logs/${DEBUGGER_NAME}/debug`, + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: this.maxSize, + maxFiles: this.maxFiles, + level: 'debug', + }) + ); + } + + if (this.writeIntoConsole) { + transports.push(new winston.transports.Console()); + } + + const loggerOptions: LoggerOptions = { + format: winston.format.combine( + winston.format.timestamp(), + winston.format.prettyPrint() + ), + transports, + }; + + return loggerOptions; + } +} diff --git a/src/common/debugger/services/debugger.service.ts b/src/common/debugger/services/debugger.service.ts new file mode 100644 index 0000000..23b9a7d --- /dev/null +++ b/src/common/debugger/services/debugger.service.ts @@ -0,0 +1,53 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Logger } from 'winston'; +import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; +import { IDebuggerLog } from 'src/common/debugger/interfaces/debugger.interface'; +import { IDebuggerService } from 'src/common/debugger/interfaces/debugger.service.interface'; + +@Injectable() +export class DebuggerService implements IDebuggerService { + constructor( + @Inject(WINSTON_MODULE_PROVIDER) + private readonly logger: Logger + ) {} + + info(requestId: string, log: IDebuggerLog, data?: any): void { + this.logger.info(log.description, { + _id: requestId, + class: log.class, + function: log.function, + path: log.path, + data, + }); + } + + debug(requestId: string, log: IDebuggerLog, data?: any): void { + this.logger.debug(log.description, { + _id: requestId, + class: log.class, + function: log.function, + path: log.path, + data, + }); + } + + warn(requestId: string, log: IDebuggerLog, data?: any): void { + this.logger.warn(log.description, { + _id: requestId, + class: log.class, + function: log.function, + path: log.path, + data, + }); + } + + error(requestId: string, log: IDebuggerLog, data?: any): void { + this.logger.error(log.description, { + _id: requestId, + class: log.class, + function: log.function, + path: log.path, + data, + }); + } +} diff --git a/src/common/doc/constants/doc.enum.constant.ts b/src/common/doc/constants/doc.enum.constant.ts new file mode 100644 index 0000000..0739cf1 --- /dev/null +++ b/src/common/doc/constants/doc.enum.constant.ts @@ -0,0 +1,11 @@ +export enum ENUM_DOC_REQUEST_BODY_TYPE { + JSON = 'JSON', + FORM_DATA = 'FORM_DATA', + TEXT = 'TEXT', +} + +export enum ENUM_DOC_RESPONSE_BODY_TYPE { + JSON = 'JSON', + FILE = 'FILE', + TEXT = 'TEXT', +} diff --git a/src/common/doc/decorators/doc.decorator.ts b/src/common/doc/decorators/doc.decorator.ts new file mode 100644 index 0000000..79039e2 --- /dev/null +++ b/src/common/doc/decorators/doc.decorator.ts @@ -0,0 +1,690 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiBody, + ApiConsumes, + ApiExtraModels, + ApiHeader, + ApiHeaders, + ApiParam, + ApiProduces, + ApiQuery, + ApiResponse, + ApiSecurity, + getSchemaPath, +} from '@nestjs/swagger'; +import { APP_LANGUAGE } from 'src/app/constants/app.constant'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; +import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant'; +import { + ENUM_DOC_REQUEST_BODY_TYPE, + ENUM_DOC_RESPONSE_BODY_TYPE, +} from 'src/common/doc/constants/doc.enum.constant'; +import { + IDocDefaultOptions, + IDocOfOptions, + IDocOptions, + IDocPagingOptions, +} from 'src/common/doc/interfaces/doc.interface'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { ENUM_FILE_EXCEL_MIME } from 'src/common/file/constants/file.enum.constant'; +import { FileMultipleDto } from 'src/common/file/dtos/file.multiple.dto'; +import { FileSingleDto } from 'src/common/file/dtos/file.single.dto'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { Skip } from 'src/common/request/validations/request.skip.validation'; +import { ResponseDefaultSerialization } from 'src/common/response/serializations/response.default.serialization'; +import { ResponsePagingSerialization } from 'src/common/response/serializations/response.paging.serialization'; + +export function Doc( + messagePath: string, + options?: IDocOptions +): MethodDecorator { + const docs = []; + const normDoc: IDocDefaultOptions = { + httpStatus: options?.response?.httpStatus ?? HttpStatus.OK, + messagePath, + statusCode: options?.response?.statusCode, + }; + + if (!normDoc.statusCode) { + normDoc.statusCode = normDoc.httpStatus; + } + + if (options?.request?.bodyType === ENUM_DOC_REQUEST_BODY_TYPE.FORM_DATA) { + docs.push(ApiConsumes('multipart/form-data')); + + if (options?.request?.file?.multiple) { + docs.push( + ApiBody({ + description: 'Multiple file', + type: FileMultipleDto, + }) + ); + } else if (!options?.request?.file?.multiple) { + docs.push( + ApiBody({ + description: 'Single file', + type: FileSingleDto, + }) + ); + } + } else if (options?.request?.bodyType === ENUM_DOC_REQUEST_BODY_TYPE.TEXT) { + docs.push(ApiConsumes('text/plain')); + } else { + docs.push(ApiConsumes('application/json')); + } + + if (options?.response?.bodyType === ENUM_DOC_RESPONSE_BODY_TYPE.FILE) { + docs.push(ApiProduces(ENUM_FILE_EXCEL_MIME.XLSX)); + } else if ( + options?.response?.bodyType === ENUM_DOC_RESPONSE_BODY_TYPE.TEXT + ) { + docs.push(ApiProduces('text/plain')); + } else { + docs.push(ApiProduces('application/json')); + if (options?.response?.serialization) { + normDoc.serialization = options?.response?.serialization; + } + } + docs.push(DocDefault(normDoc)); + + if (options?.request?.params) { + docs.push(...options?.request?.params.map((param) => ApiParam(param))); + } + + if (options?.request?.queries) { + docs.push(...options?.request?.queries.map((query) => ApiQuery(query))); + } + + const oneOfUnauthorized: IDocOfOptions[] = []; + const oneOfForbidden: IDocOfOptions[] = []; + + // auth + const auths = []; + if (options?.auth?.jwtRefreshToken) { + auths.push(ApiBearerAuth('refreshToken')); + oneOfUnauthorized.push({ + messagePath: 'auth.error.refreshTokenUnauthorized', + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_REFRESH_TOKEN_ERROR, + }); + } + + if (options?.auth?.jwtAccessToken) { + auths.push(ApiBearerAuth('accessToken')); + oneOfUnauthorized.push({ + messagePath: 'auth.error.accessTokenUnauthorized', + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_ACCESS_TOKEN_ERROR, + }); + oneOfForbidden.push( + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_INVALID_ERROR, + messagePath: 'auth.error.permissionForbidden', + }, + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_ACCESS_FOR_INVALID_ERROR, + messagePath: 'auth.error.accessForForbidden', + } + ); + } + + if (options?.auth?.apiKey) { + auths.push(ApiSecurity('apiKey')); + oneOfUnauthorized.push( + { + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NEEDED_ERROR, + messagePath: 'apiKey.error.keyNeeded', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR, + messagePath: 'apiKey.error.notFound', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INACTIVE_ERROR, + messagePath: 'apiKey.error.inactive', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INVALID_ERROR, + messagePath: 'apiKey.error.invalid', + } + ); + } + + if (options?.auth?.permissionToken) { + auths.push(ApiSecurity('permissionToken')); + oneOfUnauthorized.push( + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_ERROR, + messagePath: 'auth.error.permissionTokenUnauthorized', + }, + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_INVALID_ERROR, + messagePath: 'auth.error.permissionTokenInvalid', + }, + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_NOT_YOUR_ERROR, + messagePath: 'auth.error.permissionTokenNotYour', + } + ); + } + + // request headers + const requestHeaders = []; + if (options?.requestHeader?.userAgent) { + oneOfForbidden.push( + { + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_INVALID_ERROR, + messagePath: 'request.error.userAgentInvalid', + }, + { + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_BROWSER_INVALID_ERROR, + messagePath: 'request.error.userAgentBrowserInvalid', + }, + { + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_OS_INVALID_ERROR, + messagePath: 'request.error.userAgentOsInvalid', + } + ); + requestHeaders.push({ + name: 'user-agent', + description: 'User agent header', + required: true, + schema: { + example: + 'Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion', + type: 'string', + }, + }); + } + + if (options?.requestHeader?.timestamp) { + oneOfForbidden.push({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_TIMESTAMP_INVALID_ERROR, + messagePath: 'request.error.timestampInvalid', + }); + requestHeaders.push({ + name: 'x-timestamp', + description: 'Timestamp header, in microseconds', + required: true, + schema: { + example: 1662876305642, + type: 'number', + }, + }); + } + + return applyDecorators( + ApiHeader({ + name: 'x-custom-lang', + description: 'Custom language header', + required: false, + schema: { + default: APP_LANGUAGE, + example: APP_LANGUAGE, + type: 'string', + }, + }), + ApiHeaders(requestHeaders), + DocDefault({ + httpStatus: HttpStatus.SERVICE_UNAVAILABLE, + messagePath: 'http.serverError.serviceUnavailable', + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_SERVICE_UNAVAILABLE, + }), + DocDefault({ + httpStatus: HttpStatus.INTERNAL_SERVER_ERROR, + messagePath: 'http.serverError.internalServerError', + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + }), + DocDefault({ + httpStatus: HttpStatus.REQUEST_TIMEOUT, + messagePath: 'http.serverError.requestTimeout', + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_REQUEST_TIMEOUT, + }), + oneOfForbidden.length > 0 + ? DocOneOf(HttpStatus.FORBIDDEN, ...oneOfForbidden) + : Skip(), + oneOfUnauthorized.length > 0 + ? DocOneOf(HttpStatus.UNAUTHORIZED, ...oneOfUnauthorized) + : Skip(), + ...auths, + ...docs + ); +} + +export function DocPaging( + messagePath: string, + options: IDocPagingOptions +): MethodDecorator { + // paging + const docs = []; + + if (options?.request?.params) { + docs.push(...options?.request?.params.map((param) => ApiParam(param))); + } + + if (options?.request?.queries) { + docs.push(...options?.request?.queries.map((query) => ApiQuery(query))); + } + + const oneOfUnauthorized: IDocOfOptions[] = []; + const oneOfForbidden: IDocOfOptions[] = []; + + // auth + const auths = []; + if (options?.auth?.jwtRefreshToken) { + auths.push(ApiBearerAuth('refreshToken')); + oneOfUnauthorized.push({ + messagePath: 'auth.error.refreshTokenUnauthorized', + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_REFRESH_TOKEN_ERROR, + }); + } + + if (options?.auth?.jwtAccessToken) { + auths.push(ApiBearerAuth('accessToken')); + oneOfUnauthorized.push({ + messagePath: 'auth.error.accessTokenUnauthorized', + statusCode: ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_ACCESS_TOKEN_ERROR, + }); + oneOfForbidden.push( + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_INVALID_ERROR, + messagePath: 'auth.error.permissionForbidden', + }, + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_ACCESS_FOR_INVALID_ERROR, + messagePath: 'auth.error.accessForForbidden', + } + ); + } + + if (options?.auth?.apiKey) { + auths.push(ApiSecurity('apiKey')); + oneOfUnauthorized.push( + { + statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NEEDED_ERROR, + messagePath: 'apiKey.error.keyNeeded', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR, + messagePath: 'apiKey.error.notFound', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INACTIVE_ERROR, + messagePath: 'apiKey.error.inactive', + }, + { + statusCode: + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INVALID_ERROR, + messagePath: 'apiKey.error.invalid', + } + ); + } + + if (options?.auth?.permissionToken) { + auths.push(ApiSecurity('permissionToken')); + oneOfUnauthorized.push( + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_ERROR, + messagePath: 'auth.error.permissionTokenUnauthorized', + }, + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_INVALID_ERROR, + messagePath: 'auth.error.permissionTokenInvalid', + }, + { + statusCode: + ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_NOT_YOUR_ERROR, + messagePath: 'auth.error.permissionTokenNotYour', + } + ); + } + + // request headers + const requestHeaders = []; + if (options?.requestHeader?.userAgent) { + oneOfForbidden.push( + { + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_INVALID_ERROR, + messagePath: 'request.error.userAgentInvalid', + }, + { + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_BROWSER_INVALID_ERROR, + messagePath: 'request.error.userAgentBrowserInvalid', + }, + { + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_OS_INVALID_ERROR, + messagePath: 'request.error.userAgentOsInvalid', + } + ); + requestHeaders.push({ + name: 'user-agent', + description: 'User agent header', + required: true, + schema: { + example: + 'Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion', + type: 'string', + }, + }); + } + + if (options?.requestHeader?.timestamp) { + oneOfForbidden.push({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_TIMESTAMP_INVALID_ERROR, + messagePath: 'request.error.timestampInvalid', + }); + requestHeaders.push({ + name: 'x-timestamp', + description: 'Timestamp header, in microseconds', + required: true, + schema: { + example: 1662876305642, + type: 'number', + }, + }); + } + + return applyDecorators( + // paging + ApiConsumes('application/json'), + ApiExtraModels(ResponsePagingSerialization), + ApiExtraModels(options.response.serialization), + ApiResponse({ + status: HttpStatus.OK, + schema: { + allOf: [ + { $ref: getSchemaPath(ResponsePagingSerialization) }, + ], + properties: { + message: { + example: messagePath, + }, + statusCode: { + type: 'number', + example: options.response.statusCode ?? HttpStatus.OK, + }, + data: { + type: 'array', + items: { + $ref: getSchemaPath(options.response.serialization), + }, + }, + }, + }, + }), + ApiQuery({ + name: 'search', + required: false, + allowEmptyValue: true, + type: 'string', + description: + 'Search will base on _availableSearch with rule contains, and case insensitive', + }), + ApiQuery({ + name: 'perPage', + required: false, + allowEmptyValue: true, + example: 20, + type: 'number', + description: 'Data per page', + }), + ApiQuery({ + name: 'page', + required: false, + allowEmptyValue: true, + example: 1, + type: 'number', + description: 'page number', + }), + ApiQuery({ + name: 'orderBy', + required: false, + allowEmptyValue: true, + example: 'createdAt', + type: 'string', + description: 'Order by base on _availableOrderBy', + }), + ApiQuery({ + name: 'orderDirection', + required: false, + allowEmptyValue: true, + example: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + enum: ENUM_PAGINATION_ORDER_DIRECTION_TYPE, + type: 'string', + description: 'Order direction base on _availableOrderDirection', + }), + + // default + ApiHeader({ + name: 'x-custom-lang', + description: 'Custom language header', + required: false, + schema: { + default: APP_LANGUAGE, + example: APP_LANGUAGE, + type: 'string', + }, + }), + ApiHeaders(requestHeaders), + DocDefault({ + httpStatus: HttpStatus.SERVICE_UNAVAILABLE, + messagePath: 'http.serverError.serviceUnavailable', + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_SERVICE_UNAVAILABLE, + }), + DocDefault({ + httpStatus: HttpStatus.INTERNAL_SERVER_ERROR, + messagePath: 'http.serverError.internalServerError', + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + }), + DocDefault({ + httpStatus: HttpStatus.REQUEST_TIMEOUT, + messagePath: 'http.serverError.requestTimeout', + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_REQUEST_TIMEOUT, + }), + oneOfForbidden.length > 0 + ? DocOneOf(HttpStatus.FORBIDDEN, ...oneOfForbidden) + : Skip(), + oneOfUnauthorized.length > 0 + ? DocOneOf(HttpStatus.UNAUTHORIZED, ...oneOfUnauthorized) + : Skip(), + ...auths, + ...docs + ); +} + +export function DocDefault(options: IDocDefaultOptions): MethodDecorator { + const docs = []; + const schema: Record = { + allOf: [{ $ref: getSchemaPath(ResponseDefaultSerialization) }], + properties: { + message: { + example: options.messagePath, + }, + statusCode: { + type: 'number', + example: options.statusCode, + }, + }, + }; + + if (options.serialization) { + docs.push(ApiExtraModels(options.serialization)); + schema.properties = { + ...schema.properties, + data: { + $ref: getSchemaPath(options.serialization), + }, + }; + } + + return applyDecorators( + ApiExtraModels(ResponseDefaultSerialization), + ApiResponse({ + status: options.httpStatus, + schema, + }), + ...docs + ); +} + +export function DocOneOf( + httpStatus: HttpStatus, + ...documents: IDocOfOptions[] +): MethodDecorator { + const docs = []; + const oneOf = []; + + for (const doc of documents) { + const oneOfSchema: Record = { + allOf: [{ $ref: getSchemaPath(ResponseDefaultSerialization) }], + properties: { + message: { + example: doc.messagePath, + }, + statusCode: { + type: 'number', + example: doc.statusCode ?? HttpStatus.OK, + }, + }, + }; + + if (doc.serialization) { + docs.push(ApiExtraModels(doc.serialization)); + oneOfSchema.properties = { + ...oneOfSchema.properties, + data: { + $ref: getSchemaPath(doc.serialization), + }, + }; + } + + oneOf.push(oneOfSchema); + } + + return applyDecorators( + ApiExtraModels(ResponseDefaultSerialization), + ApiResponse({ + status: httpStatus, + schema: { + oneOf, + }, + }), + ...docs + ); +} + +export function DocAnyOf( + httpStatus: HttpStatus, + ...documents: IDocOfOptions[] +): MethodDecorator { + const docs = []; + const anyOf = []; + + for (const doc of documents) { + const anyOfSchema: Record = { + allOf: [{ $ref: getSchemaPath(ResponseDefaultSerialization) }], + properties: { + message: { + example: doc.messagePath, + }, + statusCode: { + type: 'number', + example: doc.statusCode ?? HttpStatus.OK, + }, + }, + }; + + if (doc.serialization) { + docs.push(ApiExtraModels(doc.serialization)); + anyOfSchema.properties = { + ...anyOfSchema.properties, + data: { + $ref: getSchemaPath(doc.serialization), + }, + }; + } + + anyOf.push(anyOfSchema); + } + + return applyDecorators( + ApiExtraModels(ResponseDefaultSerialization), + ApiResponse({ + status: httpStatus, + schema: { + anyOf, + }, + }), + ...docs + ); +} + +export function DocAllOf( + httpStatus: HttpStatus, + ...documents: IDocOfOptions[] +): MethodDecorator { + const docs = []; + const allOf = []; + + for (const doc of documents) { + const allOfSchema: Record = { + allOf: [{ $ref: getSchemaPath(ResponseDefaultSerialization) }], + properties: { + message: { + example: doc.messagePath, + }, + statusCode: { + type: 'number', + example: doc.statusCode ?? HttpStatus.OK, + }, + }, + }; + + if (doc.serialization) { + docs.push(ApiExtraModels(doc.serialization)); + allOfSchema.properties = { + ...allOfSchema.properties, + data: { + $ref: getSchemaPath(doc.serialization), + }, + }; + } + + allOf.push(allOfSchema); + } + + return applyDecorators( + ApiExtraModels(ResponseDefaultSerialization), + ApiResponse({ + status: httpStatus, + schema: { + allOf, + }, + }), + ...docs + ); +} diff --git a/src/common/doc/interfaces/doc.interface.ts b/src/common/doc/interfaces/doc.interface.ts new file mode 100644 index 0000000..558a0ea --- /dev/null +++ b/src/common/doc/interfaces/doc.interface.ts @@ -0,0 +1,66 @@ +import { HttpStatus } from '@nestjs/common'; +import { ApiParamOptions, ApiQueryOptions } from '@nestjs/swagger'; +import { ClassConstructor } from 'class-transformer'; +import { + ENUM_DOC_REQUEST_BODY_TYPE, + ENUM_DOC_RESPONSE_BODY_TYPE, +} from 'src/common/doc/constants/doc.enum.constant'; + +export interface IDocOfOptions { + messagePath: string; + statusCode: number; + serialization?: ClassConstructor; +} + +export interface IDocDefaultOptions { + httpStatus: HttpStatus; + messagePath: string; + statusCode: number; + serialization?: ClassConstructor; +} + +export interface IDocOptions { + auth?: IDocAuthOptions; + requestHeader?: IDocRequestHeaderOptions; + response?: IDocResponseOptions; + request?: IDocRequestOptions; +} + +export interface IDocPagingOptions + extends Omit, 'response' | 'request'> { + response: IDocPagingResponseOptions; + request?: Omit; +} + +export interface IDocResponseOptions { + statusCode?: number; + httpStatus?: HttpStatus; + bodyType?: ENUM_DOC_RESPONSE_BODY_TYPE; + serialization?: ClassConstructor; +} + +export interface IDocPagingResponseOptions + extends Pick, 'statusCode'> { + serialization: ClassConstructor; +} + +export interface IDocAuthOptions { + jwtAccessToken?: boolean; + jwtRefreshToken?: boolean; + apiKey?: boolean; + permissionToken?: boolean; +} + +export interface IDocRequestHeaderOptions { + userAgent?: boolean; + timestamp?: boolean; +} + +export interface IDocRequestOptions { + params?: ApiParamOptions[]; + queries?: ApiQueryOptions[]; + bodyType?: ENUM_DOC_REQUEST_BODY_TYPE; + file?: { + multiple: boolean; + }; +} diff --git a/src/common/error/constants/error.constant.ts b/src/common/error/constants/error.constant.ts new file mode 100644 index 0000000..64a68c3 --- /dev/null +++ b/src/common/error/constants/error.constant.ts @@ -0,0 +1,2 @@ +export const ERROR_CLASS_META_KEY = 'ErrorMetaClassKey'; +export const ERROR_FUNCTION_META_KEY = 'ErrorMetaFunctionKey'; diff --git a/src/common/error/constants/error.enum.constant.ts b/src/common/error/constants/error.enum.constant.ts new file mode 100644 index 0000000..3caccc0 --- /dev/null +++ b/src/common/error/constants/error.enum.constant.ts @@ -0,0 +1,4 @@ +export enum ERROR_TYPE { + DEFAULT = 'DEFAULT', + IMPORT = 'IMPORT', +} diff --git a/src/common/error/constants/error.status-code.constant.ts b/src/common/error/constants/error.status-code.constant.ts new file mode 100644 index 0000000..ccb99bd --- /dev/null +++ b/src/common/error/constants/error.status-code.constant.ts @@ -0,0 +1,5 @@ +export enum ENUM_ERROR_STATUS_CODE_ERROR { + ERROR_UNKNOWN = 5990, + ERROR_SERVICE_UNAVAILABLE = 5991, + ERROR_REQUEST_TIMEOUT = 5992, +} diff --git a/src/common/error/decorators/error.decorator.ts b/src/common/error/decorators/error.decorator.ts new file mode 100644 index 0000000..7b14acb --- /dev/null +++ b/src/common/error/decorators/error.decorator.ts @@ -0,0 +1,12 @@ +import { applyDecorators, SetMetadata } from '@nestjs/common'; +import { + ERROR_CLASS_META_KEY, + ERROR_FUNCTION_META_KEY, +} from 'src/common/error/constants/error.constant'; + +export function ErrorMeta(cls: string, func: string): MethodDecorator { + return applyDecorators( + SetMetadata(ERROR_CLASS_META_KEY, cls), + SetMetadata(ERROR_FUNCTION_META_KEY, func) + ); +} diff --git a/src/common/error/error.module.ts b/src/common/error/error.module.ts new file mode 100644 index 0000000..b514375 --- /dev/null +++ b/src/common/error/error.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { APP_FILTER, APP_GUARD } from '@nestjs/core'; +import { ErrorHttpFilter } from './filters/error.http.filter'; +import { ErrorMetaGuard } from './guards/error.meta.guard'; + +@Module({ + controllers: [], + providers: [ + { + provide: APP_FILTER, + useClass: ErrorHttpFilter, + }, + { + provide: APP_GUARD, + useClass: ErrorMetaGuard, + }, + ], + imports: [], +}) +export class ErrorModule {} diff --git a/src/common/error/filters/error.http.filter.ts b/src/common/error/filters/error.http.filter.ts new file mode 100644 index 0000000..61fabc7 --- /dev/null +++ b/src/common/error/filters/error.http.filter.ts @@ -0,0 +1,186 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + Optional, +} from '@nestjs/common'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { ConfigService } from '@nestjs/config'; +import { ValidationError } from 'class-validator'; +import { Response } from 'express'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { DebuggerService } from 'src/common/debugger/services/debugger.service'; +import { ERROR_TYPE } from 'src/common/error/constants/error.enum.constant'; +import { + IErrorException, + IErrors, + IErrorsImport, + IValidationErrorImport, +} from 'src/common/error/interfaces/error.interface'; +import { ErrorMetadataSerialization } from 'src/common/error/serializations/error.serialization'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { + IMessage, + IMessageOptionsProperties, +} from 'src/common/message/interfaces/message.interface'; +import { MessageService } from 'src/common/message/services/message.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +// If we throw error with HttpException, there will always return object +// The exception filter only catch HttpException +@Catch() +export class ErrorHttpFilter implements ExceptionFilter { + private readonly appDefaultLanguage: string[]; + + constructor( + @Optional() private readonly debuggerService: DebuggerService, + private readonly configService: ConfigService, + private readonly messageService: MessageService, + private readonly helperDateService: HelperDateService + ) { + this.appDefaultLanguage = + this.configService.get('app.language'); + } + + async catch(exception: unknown, host: ArgumentsHost): Promise { + const ctx: HttpArgumentsHost = host.switchToHttp(); + const response: Response = ctx.getResponse(); + const request: IRequestApp = ctx.getRequest(); + + // get request headers + const __customLang: string[] = + request.__customLang ?? this.appDefaultLanguage; + const __class = request.__class ?? ErrorHttpFilter.name; + const __function = request.__function ?? this.catch.name; + const __requestId = request.__id ?? DatabaseDefaultUUID(); + const __path = request.path; + const __timestamp = + request.__xTimestamp ?? + request.__timestamp ?? + this.helperDateService.timestamp(); + const __timezone = + request.__timezone ?? + Intl.DateTimeFormat().resolvedOptions().timeZone; + const __version = + request.__version ?? + this.configService.get('app.versioning.version'); + const __repoVersion = + request.__repoVersion ?? + this.configService.get('app.repoVersion'); + + // Debugger + try { + this.debuggerService.error( + request?.__id ? request.__id : ErrorHttpFilter.name, + { + description: + exception instanceof Error + ? exception.message + : exception.toString(), + class: __class ?? ErrorHttpFilter.name, + function: __function ?? this.catch.name, + path: __path, + }, + exception + ); + } catch (err: unknown) {} + + // set default + let statusHttp: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR; + let messagePath = `http.${statusHttp}`; + let statusCode = HttpStatus.INTERNAL_SERVER_ERROR; + let _error: string = undefined; + let errors: IErrors[] | IErrorsImport[] = undefined; + let messageProperties: IMessageOptionsProperties = undefined; + let data: Record = undefined; + let metadata: ErrorMetadataSerialization = { + languages: __customLang, + timestamp: __timestamp, + timezone: __timezone, + requestId: __requestId, + path: __path, + version: __version, + repoVersion: __repoVersion, + }; + if (exception instanceof HttpException) { + // Restructure + const responseException = exception.getResponse(); + statusHttp = exception.getStatus(); + messagePath = `http.${statusHttp}`; + statusCode = exception.getStatus(); + + if (this.isErrorException(responseException)) { + const { _metadata } = responseException; + + statusCode = responseException.statusCode; + messagePath = responseException.message; + data = responseException.data; + messageProperties = + _metadata?.customProperty?.messageProperties; + delete _metadata?.customProperty; + + metadata = { + ...metadata, + ..._metadata, + }; + + if (responseException.errors?.length > 0) { + errors = + responseException._errorType === ERROR_TYPE.IMPORT + ? await this.messageService.getImportErrorsMessage( + responseException.errors as IValidationErrorImport[], + __customLang + ) + : await this.messageService.getRequestErrorsMessage( + responseException.errors as ValidationError[], + __customLang + ); + } + + if (!responseException._error) { + _error = + typeof responseException._error !== 'string' + ? JSON.stringify(responseException._error) + : responseException._error; + } + } + } + + const message: string | IMessage = await this.messageService.get( + messagePath, + { + customLanguages: __customLang, + properties: messageProperties, + } + ); + + const responseBody = { + statusCode, + message, + errors, + _error, + _metadata: metadata, + data, + }; + + response + .setHeader('x-custom-lang', __customLang) + .setHeader('x-timestamp', __timestamp) + .setHeader('x-timezone', __timezone) + .setHeader('x-request-id', __requestId) + .setHeader('x-version', __version) + .setHeader('x-repo-version', __repoVersion) + .status(statusHttp) + .json(responseBody); + + return; + } + + isErrorException(obj: any): obj is IErrorException { + return typeof obj === 'object' + ? 'statusCode' in obj && 'message' in obj + : false; + } +} diff --git a/src/common/error/guards/error.meta.guard.ts b/src/common/error/guards/error.meta.guard.ts new file mode 100644 index 0000000..73c277c --- /dev/null +++ b/src/common/error/guards/error.meta.guard.ts @@ -0,0 +1,31 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { + ERROR_CLASS_META_KEY, + ERROR_FUNCTION_META_KEY, +} from 'src/common/error/constants/error.constant'; + +@Injectable() +export class ErrorMetaGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const cls = this.reflector.get( + ERROR_CLASS_META_KEY, + context.getHandler() + ); + const func = this.reflector.get( + ERROR_FUNCTION_META_KEY, + context.getHandler() + ); + + const className = context.getClass().name; + const methodKey = context.getHandler().name; + + request.__class = cls ?? className; + request.__function = func ?? methodKey; + + return true; + } +} diff --git a/src/common/error/interfaces/error.interface.ts b/src/common/error/interfaces/error.interface.ts new file mode 100644 index 0000000..c77afa9 --- /dev/null +++ b/src/common/error/interfaces/error.interface.ts @@ -0,0 +1,48 @@ +import { ValidationError } from 'class-validator'; +import { ERROR_TYPE } from 'src/common/error/constants/error.enum.constant'; +import { IMessage } from 'src/common/message/interfaces/message.interface'; +import { IResponseCustomPropertyMetadata } from 'src/common/response/interfaces/response.interface'; + +// error default +export interface IErrors { + readonly message: string | IMessage; + readonly property: string; +} + +// error import +export interface IErrorsImport { + row: number; + file?: string; + errors: IErrors[]; +} + +export interface IValidationErrorImport extends Omit { + errors: ValidationError[]; +} + +// error exception + +export type IErrorCustomPropertyMetadata = Pick< + IResponseCustomPropertyMetadata, + 'messageProperties' +>; + +export interface IErrorMetadata { + customProperty?: IErrorCustomPropertyMetadata; + [key: string]: any; +} + +export interface IErrorException { + statusCode: number; + message: string; + errors?: ValidationError[] | IValidationErrorImport[]; + data?: Record; + _error?: string; + _errorType?: ERROR_TYPE; + _metadata?: IErrorMetadata; +} + +export interface IErrorHttpFilter + extends Omit { + message: string | IMessage; +} diff --git a/src/common/error/serializations/error.serialization.ts b/src/common/error/serializations/error.serialization.ts new file mode 100644 index 0000000..fea3c03 --- /dev/null +++ b/src/common/error/serializations/error.serialization.ts @@ -0,0 +1,3 @@ +import { ResponseMetadataSerialization } from 'src/common/response/serializations/response.default.serialization'; + +export class ErrorMetadataSerialization extends ResponseMetadataSerialization {} diff --git a/src/common/file/constants/file.constant.ts b/src/common/file/constants/file.constant.ts new file mode 100644 index 0000000..e71ba76 --- /dev/null +++ b/src/common/file/constants/file.constant.ts @@ -0,0 +1,2 @@ +export const FILE_CUSTOM_MAX_SIZE_META_KEY = 'FileCustomMaxSizeMetaKey'; +export const FILE_CUSTOM_MAX_FILES_META_KEY = 'FileCustomMaxFilesMetaKey'; diff --git a/src/common/file/constants/file.enum.constant.ts b/src/common/file/constants/file.enum.constant.ts new file mode 100644 index 0000000..99ff034 --- /dev/null +++ b/src/common/file/constants/file.enum.constant.ts @@ -0,0 +1,29 @@ +export enum ENUM_FILE_IMAGE_MIME { + JPG = 'image/jpg', + JPEG = 'image/jpeg', + PNG = 'image/png', +} + +export enum ENUM_FILE_EXCEL_MIME { + XLS = 'application/vnd.ms-excel', + XLSX = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + CSV = 'text/csv', +} + +export enum ENUM_FILE_AUDIO_MIME { + MPEG = 'audio/mpeg', + MP3 = 'audio/mp3', + MP4 = 'audio/mp4', +} + +export enum ENUM_FILE_VIDEO_MIME { + MP4 = 'video/mp4', + APPLICATION_MP4 = 'application/mp4', +} + +export enum ENUM_FILE_TYPE { + AUDIO = 'audio', + IMAGE = 'image', + EXCEL = 'excel', + VIDEO = 'video', +} diff --git a/src/common/file/constants/file.status-code.constant.ts b/src/common/file/constants/file.status-code.constant.ts new file mode 100644 index 0000000..cacda03 --- /dev/null +++ b/src/common/file/constants/file.status-code.constant.ts @@ -0,0 +1,8 @@ +export enum ENUM_FILE_STATUS_CODE_ERROR { + FILE_NEEDED_ERROR = 5950, + FILE_MAX_SIZE_ERROR = 5951, + FILE_EXTENSION_ERROR = 5952, + FILE_MAX_FILES_ERROR = 5953, + FILE_VALIDATION_DTO_ERROR = 5954, + FILE_NEED_EXTRACT_FIRST_ERROR = 5955, +} diff --git a/src/common/file/decorators/file.decorator.ts b/src/common/file/decorators/file.decorator.ts new file mode 100644 index 0000000..225ec96 --- /dev/null +++ b/src/common/file/decorators/file.decorator.ts @@ -0,0 +1,45 @@ +import { + applyDecorators, + createParamDecorator, + ExecutionContext, + SetMetadata, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express'; +import { + FILE_CUSTOM_MAX_FILES_META_KEY, + FILE_CUSTOM_MAX_SIZE_META_KEY, +} from 'src/common/file/constants/file.constant'; +import { FileCustomMaxFilesInterceptor } from 'src/common/file/interceptors/file.custom-max-files.interceptor'; +import { FileCustomMaxSizeInterceptor } from 'src/common/file/interceptors/file.custom-max-size.interceptor'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function UploadFileSingle(field: string): MethodDecorator { + return applyDecorators(UseInterceptors(FileInterceptor(field))); +} + +export function UploadFileMultiple(field: string): MethodDecorator { + return applyDecorators(UseInterceptors(FilesInterceptor(field))); +} + +export function FileCustomMaxFile(customMaxFiles: number): MethodDecorator { + return applyDecorators( + UseInterceptors(FileCustomMaxFilesInterceptor), + SetMetadata(FILE_CUSTOM_MAX_FILES_META_KEY, customMaxFiles) + ); +} + +export function FileCustomMaxSize(customMaxSize: string): MethodDecorator { + return applyDecorators( + UseInterceptors(FileCustomMaxSizeInterceptor), + SetMetadata(FILE_CUSTOM_MAX_SIZE_META_KEY, customMaxSize) + ); +} + +export const FilePartNumber: () => ParameterDecorator = createParamDecorator( + (data: string, ctx: ExecutionContext): number => { + const request = ctx.switchToHttp().getRequest() as IRequestApp; + const { headers } = request; + return headers['x-part-number'] ? Number(headers['x-part-number']) : 0; + } +); diff --git a/src/common/file/dtos/file.multiple.dto.ts b/src/common/file/dtos/file.multiple.dto.ts new file mode 100644 index 0000000..080b08d --- /dev/null +++ b/src/common/file/dtos/file.multiple.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class FileMultipleDto { + @ApiProperty({ type: 'array', items: { type: 'string', format: 'binary' } }) + files: any[]; +} diff --git a/src/common/file/dtos/file.single.dto.ts b/src/common/file/dtos/file.single.dto.ts new file mode 100644 index 0000000..d3089a8 --- /dev/null +++ b/src/common/file/dtos/file.single.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class FileSingleDto { + @ApiProperty({ type: 'string', format: 'binary' }) + file: any; +} diff --git a/src/common/file/interceptors/file.custom-max-files.interceptor.ts b/src/common/file/interceptors/file.custom-max-files.interceptor.ts new file mode 100644 index 0000000..7b9af8a --- /dev/null +++ b/src/common/file/interceptors/file.custom-max-files.interceptor.ts @@ -0,0 +1,35 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { FILE_CUSTOM_MAX_FILES_META_KEY } from 'src/common/file/constants/file.constant'; +import { Reflector } from '@nestjs/core'; + +@Injectable() +export class FileCustomMaxFilesInterceptor implements NestInterceptor { + constructor(private readonly reflector: Reflector) {} + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise> { + if (context.getType() === 'http') { + const ctx: HttpArgumentsHost = context.switchToHttp(); + const request = ctx.getRequest(); + + const maxFiles: number = this.reflector.get( + FILE_CUSTOM_MAX_FILES_META_KEY, + context.getHandler() + ); + request.__customMaxFiles = maxFiles; + + return next.handle(); + } + + return next.handle(); + } +} diff --git a/src/common/file/interceptors/file.custom-max-size.interceptor.ts b/src/common/file/interceptors/file.custom-max-size.interceptor.ts new file mode 100644 index 0000000..96403cc --- /dev/null +++ b/src/common/file/interceptors/file.custom-max-size.interceptor.ts @@ -0,0 +1,35 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { FILE_CUSTOM_MAX_SIZE_META_KEY } from 'src/common/file/constants/file.constant'; +import { Reflector } from '@nestjs/core'; + +@Injectable() +export class FileCustomMaxSizeInterceptor implements NestInterceptor { + constructor(private readonly reflector: Reflector) {} + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise> { + if (context.getType() === 'http') { + const ctx: HttpArgumentsHost = context.switchToHttp(); + const request = ctx.getRequest(); + + const customSize: string = this.reflector.get( + FILE_CUSTOM_MAX_SIZE_META_KEY, + context.getHandler() + ); + request.__customMaxFileSize = customSize; + + return next.handle(); + } + + return next.handle(); + } +} diff --git a/src/common/file/interfaces/file.interface.ts b/src/common/file/interfaces/file.interface.ts new file mode 100644 index 0000000..aab5df9 --- /dev/null +++ b/src/common/file/interfaces/file.interface.ts @@ -0,0 +1,6 @@ +export type IFile = Express.Multer.File; + +export type IFileExtract> = IFile & { + extract: Record[]; + dto?: T[]; +}; diff --git a/src/common/file/pipes/file.extract.pipe.ts b/src/common/file/pipes/file.extract.pipe.ts new file mode 100644 index 0000000..b84d153 --- /dev/null +++ b/src/common/file/pipes/file.extract.pipe.ts @@ -0,0 +1,62 @@ +import { Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; +import { PipeTransform } from '@nestjs/common/interfaces'; +import { ENUM_FILE_EXCEL_MIME } from 'src/common/file/constants/file.enum.constant'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/constants/file.status-code.constant'; +import { IFile, IFileExtract } from 'src/common/file/interfaces/file.interface'; +import { HelperFileService } from 'src/common/helper/services/helper.file.service'; + +// only for excel +@Injectable() +export class FileExtractPipe implements PipeTransform { + constructor(private readonly helperFileService: HelperFileService) {} + + async transform( + value: IFile | IFile[] + ): Promise { + if (!value) { + return; + } + + if (Array.isArray(value)) { + const extracts: IFileExtract[] = []; + + for (const val of value) { + await this.validate(val.mimetype); + + const extract: IFileExtract = await this.extract(val); + extracts.push(extract); + } + + return extracts; + } + + const file: IFile = value as IFile; + await this.validate(file.mimetype); + + return this.extract(file); + } + + async validate(mimetype: string): Promise { + if ( + !Object.values(ENUM_FILE_EXCEL_MIME).find( + (val) => val === mimetype.toLowerCase() + ) + ) { + throw new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_EXTENSION_ERROR, + message: 'file.error.mimeInvalid', + }); + } + } + + async extract(value: IFile): Promise { + const extract = this.helperFileService.readExcelFromBuffer( + value.buffer + ); + + return { + ...value, + extract, + }; + } +} diff --git a/src/common/file/pipes/file.max-files.pipe.ts b/src/common/file/pipes/file.max-files.pipe.ts new file mode 100644 index 0000000..9bda249 --- /dev/null +++ b/src/common/file/pipes/file.max-files.pipe.ts @@ -0,0 +1,145 @@ +import { + PipeTransform, + Injectable, + UnprocessableEntityException, + Scope, + Inject, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { REQUEST } from '@nestjs/core'; +import { Request } from 'express'; +import { IFile } from 'src/common/file/interfaces/file.interface'; +import { ENUM_FILE_STATUS_CODE_ERROR } from '../constants/file.status-code.constant'; + +// only for multiple upload + +@Injectable({ scope: Scope.REQUEST }) +export class FileMaxFilesImagePipe implements PipeTransform { + private readonly maxFile: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFiles: number }, + private readonly configService: ConfigService + ) { + this.maxFile = this.configService.get('file.image.maxFiles'); + } + + async transform(value: IFile[]): Promise { + if (!value) { + return value; + } + + await this.validate(value); + + return value; + } + + async validate(value: IFile[]): Promise { + const maxFiles = this.request.__customMaxFiles ?? this.maxFile; + if (value.length > maxFiles) { + throw new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_FILES_ERROR, + message: 'file.error.maxFiles', + }); + } + + return; + } +} + +@Injectable({ scope: Scope.REQUEST }) +export class FileMaxFilesExcelPipe implements PipeTransform { + private readonly maxFile: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFiles: number }, + private readonly configService: ConfigService + ) { + this.maxFile = this.configService.get('file.excel.maxFiles'); + } + + async transform(value: IFile[]): Promise { + await this.validate(value); + + return value; + } + + async validate(value: IFile[]): Promise { + const maxFiles = this.request.__customMaxFiles ?? this.maxFile; + + if (value.length > maxFiles) { + throw new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_FILES_ERROR, + message: 'file.error.maxFiles', + }); + } + + return; + } +} + +@Injectable({ scope: Scope.REQUEST }) +export class FileMaxFilesVideoPipe implements PipeTransform { + private readonly maxFile: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFiles: number }, + private readonly configService: ConfigService + ) { + this.maxFile = this.configService.get('file.video.maxFiles'); + } + + async transform(value: IFile[]): Promise { + await this.validate(value); + + return value; + } + + async validate(value: IFile[]): Promise { + const maxFiles = this.request.__customMaxFiles ?? this.maxFile; + + if (value.length > maxFiles) { + throw new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_FILES_ERROR, + message: 'file.error.maxFiles', + }); + } + + return; + } +} + +@Injectable({ scope: Scope.REQUEST }) +export class FileMaxFilesAudioPipe implements PipeTransform { + private readonly maxFile: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFiles: number }, + private readonly configService: ConfigService + ) { + this.maxFile = this.configService.get('file.audio.maxFiles'); + } + + async transform(value: IFile[]): Promise { + await this.validate(value); + + return value; + } + + async validate(value: IFile[]): Promise { + const maxFiles = this.request.__customMaxFiles ?? this.maxFile; + + if (value.length > maxFiles) { + throw new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_FILES_ERROR, + message: 'file.error.maxFiles', + }); + } + + return; + } +} diff --git a/src/common/file/pipes/file.required.pipe.ts b/src/common/file/pipes/file.required.pipe.ts new file mode 100644 index 0000000..b2f3948 --- /dev/null +++ b/src/common/file/pipes/file.required.pipe.ts @@ -0,0 +1,27 @@ +import { + PipeTransform, + Injectable, + UnprocessableEntityException, +} from '@nestjs/common'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/constants/file.status-code.constant'; +import { IFile } from 'src/common/file/interfaces/file.interface'; + +@Injectable() +export class FileRequiredPipe implements PipeTransform { + async transform(value: IFile | IFile[]): Promise { + await this.validate(value); + + return value; + } + + async validate(value: IFile | IFile[]): Promise { + if (!value || (Array.isArray(value) && value.length === 0)) { + throw new UnprocessableEntityException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_NEEDED_ERROR, + message: 'file.error.notFound', + }); + } + + return; + } +} diff --git a/src/common/file/pipes/file.size.pipe.ts b/src/common/file/pipes/file.size.pipe.ts new file mode 100644 index 0000000..a671569 --- /dev/null +++ b/src/common/file/pipes/file.size.pipe.ts @@ -0,0 +1,200 @@ +import { + PipeTransform, + Injectable, + PayloadTooLargeException, + Scope, + Inject, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { REQUEST } from '@nestjs/core'; +import { IFile } from 'src/common/file/interfaces/file.interface'; +import { HelperFileService } from 'src/common/helper/services/helper.file.service'; +import { ENUM_FILE_STATUS_CODE_ERROR } from '../constants/file.status-code.constant'; + +@Injectable({ scope: Scope.REQUEST }) +export class FileSizeImagePipe implements PipeTransform { + private readonly maxSize: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFileSize: string }, + private readonly configService: ConfigService, + private readonly helperFileService: HelperFileService + ) { + this.maxSize = this.configService.get('file.image.maxFileSize'); + } + + async transform(value: IFile | IFile[]): Promise { + if (!value) { + return; + } + + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.size); + } + + return value; + } + + const file: IFile = value as IFile; + await this.validate(file.size); + + return value; + } + + async validate(size: number): Promise { + const maxSizeInBytes = this.request.__customMaxFileSize + ? this.helperFileService.convertToBytes( + this.request.__customMaxFileSize + ) + : this.maxSize; + + if (size > maxSizeInBytes) { + throw new PayloadTooLargeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_SIZE_ERROR, + message: 'file.error.maxSize', + }); + } + + return; + } +} + +@Injectable({ scope: Scope.REQUEST }) +export class FileSizeExcelPipe implements PipeTransform { + private readonly maxSize: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFileSize: string }, + private readonly configService: ConfigService, + private readonly helperFileService: HelperFileService + ) { + this.maxSize = this.configService.get('file.excel.maxFileSize'); + } + + async transform(value: IFile | IFile[]): Promise { + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.size); + } + + return value; + } + + const file: IFile = value as IFile; + await this.validate(file.size); + + return value; + } + + async validate(size: number): Promise { + const maxSizeInBytes = this.request.__customMaxFileSize + ? this.helperFileService.convertToBytes( + this.request.__customMaxFileSize + ) + : this.maxSize; + + if (size > maxSizeInBytes) { + throw new PayloadTooLargeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_SIZE_ERROR, + message: 'file.error.maxSize', + }); + } + + return; + } +} + +@Injectable({ scope: Scope.REQUEST }) +export class FileSizeVideoPipe implements PipeTransform { + private readonly maxSize: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFileSize: string }, + private readonly configService: ConfigService, + private readonly helperFileService: HelperFileService + ) { + this.maxSize = this.configService.get('file.video.maxFileSize'); + } + + async transform(value: IFile | IFile[]): Promise { + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.size); + } + + return value; + } + + const file: IFile = value as IFile; + await this.validate(file.size); + + return value; + } + + async validate(size: number): Promise { + const maxSizeInBytes = this.request.__customMaxFileSize + ? this.helperFileService.convertToBytes( + this.request.__customMaxFileSize + ) + : this.maxSize; + + if (size > maxSizeInBytes) { + throw new PayloadTooLargeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_SIZE_ERROR, + message: 'file.error.maxSize', + }); + } + + return; + } +} + +@Injectable({ scope: Scope.REQUEST }) +export class FileSizeAudioPipe implements PipeTransform { + private readonly maxSize: number; + + constructor( + @Inject(REQUEST) + private readonly request: Request & { __customMaxFileSize: string }, + private readonly configService: ConfigService, + private readonly helperFileService: HelperFileService + ) { + this.maxSize = this.configService.get('file.audio.maxFileSize'); + } + + async transform(value: IFile | IFile[]): Promise { + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.size); + } + + return value; + } + + const file: IFile = value as IFile; + await this.validate(file.size); + + return value; + } + + async validate(size: number): Promise { + const maxSizeInBytes = this.request.__customMaxFileSize + ? this.helperFileService.convertToBytes( + this.request.__customMaxFileSize + ) + : this.maxSize; + + if (size > maxSizeInBytes) { + throw new PayloadTooLargeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_MAX_SIZE_ERROR, + message: 'file.error.maxSize', + }); + } + + return; + } +} diff --git a/src/common/file/pipes/file.type.pipe.ts b/src/common/file/pipes/file.type.pipe.ts new file mode 100644 index 0000000..8067a55 --- /dev/null +++ b/src/common/file/pipes/file.type.pipe.ts @@ -0,0 +1,149 @@ +import { + PipeTransform, + Injectable, + UnsupportedMediaTypeException, +} from '@nestjs/common'; +import { + ENUM_FILE_AUDIO_MIME, + ENUM_FILE_EXCEL_MIME, + ENUM_FILE_IMAGE_MIME, + ENUM_FILE_VIDEO_MIME, +} from 'src/common/file/constants/file.enum.constant'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/constants/file.status-code.constant'; +import { IFile } from 'src/common/file/interfaces/file.interface'; + +@Injectable() +export class FileTypeImagePipe implements PipeTransform { + async transform(value: IFile | IFile[]): Promise { + if (!value) { + return; + } + + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.mimetype); + } + + return value; + } + + const file = value as IFile; + await this.validate(file.mimetype); + + return value; + } + + async validate(mimetype: string): Promise { + if ( + !Object.values(ENUM_FILE_IMAGE_MIME).find( + (val) => val === mimetype.toLowerCase() + ) + ) { + throw new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_EXTENSION_ERROR, + message: 'file.error.mimeInvalid', + }); + } + + return; + } +} + +@Injectable() +export class FileTypeVideoPipe implements PipeTransform { + async transform(value: IFile | IFile[]): Promise { + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.mimetype); + } + + return value; + } + + const file = value as IFile; + await this.validate(file.mimetype); + + return value; + } + + async validate(mimetype: string): Promise { + if ( + !Object.values(ENUM_FILE_VIDEO_MIME).find( + (val) => val === mimetype.toLowerCase() + ) + ) { + throw new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_EXTENSION_ERROR, + message: 'file.error.mimeInvalid', + }); + } + + return; + } +} + +@Injectable() +export class FileTypeAudioPipe implements PipeTransform { + async transform(value: IFile | IFile[]): Promise { + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.mimetype); + } + + return value; + } + + const file = value as IFile; + await this.validate(file.mimetype); + + return value; + } + + async validate(mimetype: string): Promise { + if ( + !Object.values(ENUM_FILE_AUDIO_MIME).find( + (val) => val === mimetype.toLowerCase() + ) + ) { + throw new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_EXTENSION_ERROR, + message: 'file.error.mimeInvalid', + }); + } + + return; + } +} + +@Injectable() +export class FileTypeExcelPipe implements PipeTransform { + async transform(value: IFile | IFile[]): Promise { + if (Array.isArray(value)) { + for (const val of value) { + await this.validate(val.mimetype); + } + + return value; + } + + const file: IFile = value as IFile; + await this.validate(file.mimetype); + + return value; + } + + async validate(mimetype: string): Promise { + if ( + !Object.values(ENUM_FILE_EXCEL_MIME).find( + (val) => val === mimetype.toLowerCase() + ) + ) { + throw new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_EXTENSION_ERROR, + message: 'file.error.mimeInvalid', + }); + } + + return; + } +} diff --git a/src/common/file/pipes/file.validation.pipe.ts b/src/common/file/pipes/file.validation.pipe.ts new file mode 100644 index 0000000..a9f7994 --- /dev/null +++ b/src/common/file/pipes/file.validation.pipe.ts @@ -0,0 +1,128 @@ +import { + Injectable, + UnprocessableEntityException, + UnsupportedMediaTypeException, +} from '@nestjs/common'; +import { PipeTransform } from '@nestjs/common/interfaces'; +import { validate, ValidationError } from 'class-validator'; +import { ClassConstructor, plainToInstance } from 'class-transformer'; +import { IValidationErrorImport } from 'src/common/error/interfaces/error.interface'; +import { IFileExtract } from 'src/common/file/interfaces/file.interface'; +import { ENUM_FILE_EXCEL_MIME } from 'src/common/file/constants/file.enum.constant'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/constants/file.status-code.constant'; + +// only for excel +// must use after FileExtractPipe +@Injectable() +export class FileValidationPipe implements PipeTransform { + constructor(private readonly dto: ClassConstructor) {} + + async transform( + value: IFileExtract | IFileExtract[] + ): Promise | IFileExtract[]> { + if (!value) { + return; + } + + if (Array.isArray(value)) { + const classTransforms: IFileExtract[] = []; + for (const val of value) { + await this.validate(val); + + const classTransform: T[] = await this.transformExtract( + this.dto, + val.extract + ); + + await this.validateExtract(classTransform, val.filename); + + const classTransformMerge: IFileExtract = + await this.transformMerge(val, classTransform); + classTransforms.push(classTransformMerge); + } + + return classTransforms; + } + + const file: IFileExtract = value as IFileExtract; + await this.validate(file); + + const classTransform: T[] = await this.transformExtract( + this.dto, + file.extract + ); + + await this.validateExtract(classTransform, file.filename); + + return this.transformMerge(value, classTransform); + } + + async transformMerge( + value: IFileExtract, + classTransform: T[] + ): Promise> { + return { + ...value, + dto: classTransform, + }; + } + + async transformExtract( + classDtos: ClassConstructor, + extract: Record[] + ): Promise { + return plainToInstance(classDtos, extract); + } + + async validate(value: IFileExtract): Promise { + if ( + !Object.values(ENUM_FILE_EXCEL_MIME).find( + (val) => val === value.mimetype.toLowerCase() + ) + ) { + throw new UnsupportedMediaTypeException({ + statusCode: ENUM_FILE_STATUS_CODE_ERROR.FILE_EXTENSION_ERROR, + message: 'file.error.mimeInvalid', + }); + } else if (!value.extract) { + throw new UnprocessableEntityException({ + statusCode: + ENUM_FILE_STATUS_CODE_ERROR.FILE_NEED_EXTRACT_FIRST_ERROR, + message: 'file.error.needExtractFirst', + }); + } + + return; + } + + async validateExtract( + classTransform: T[], + filename: string + ): Promise { + const errors: IValidationErrorImport[] = []; + for (const [index, clsTransform] of classTransform.entries()) { + const validator: ValidationError[] = await validate( + clsTransform as Record + ); + if (validator.length > 0) { + errors.push({ + row: index, + file: filename, + errors: validator, + }); + } + } + + if (errors.length > 0) { + throw new UnprocessableEntityException({ + statusCode: + ENUM_FILE_STATUS_CODE_ERROR.FILE_VALIDATION_DTO_ERROR, + message: 'file.error.validationDto', + errors, + _errorType: 'import', + }); + } + + return; + } +} diff --git a/src/common/helper/constants/helper.enum.constant.ts b/src/common/helper/constants/helper.enum.constant.ts new file mode 100644 index 0000000..49a24a1 --- /dev/null +++ b/src/common/helper/constants/helper.enum.constant.ts @@ -0,0 +1,33 @@ +export enum ENUM_HELPER_DATE_FORMAT { + DATE = 'YYYY-MM-DD', + FRIENDLY_DATE = 'MMM, DD YYYY', + FRIENDLY_DATE_TIME = 'MMM, DD YYYY HH:MM:SS', + YEAR_MONTH = 'YYYY-MM', + MONTH_DATE = 'MM-DD', + ONLY_YEAR = 'YYYY', + ONLY_MONTH = 'MM', + ONLY_DATE = 'DD', + ISO_DATE = 'YYYY-MM-DDTHH:MM:SSZ', + DAY_LONG = 'dddd', + DAY_SHORT = 'ddd', + HOUR_LONG = 'HH', + HOUR_SHORT = 'H', + MINUTE_LONG = 'mm', + MINUTE_SHORT = 'm', + SECOND_LONG = 'ss', + SECOND_SHORT = 's', +} + +export enum ENUM_HELPER_DATE_DIFF { + MILIS = 'milis', + SECONDS = 'seconds', + HOURS = 'hours', + DAYS = 'days', + MINUTES = 'minutes', +} + +export enum ENUM_HELPER_FILE_TYPE { + XLSX = 'xlsx', + XLS = 'xls', + CSV = 'csv', +} diff --git a/src/common/helper/constants/helper.function.constant.ts b/src/common/helper/constants/helper.function.constant.ts new file mode 100644 index 0000000..0a0dc66 --- /dev/null +++ b/src/common/helper/constants/helper.function.constant.ts @@ -0,0 +1,5 @@ +import ms from 'ms'; + +export function seconds(msValue: string): number { + return ms(msValue) / 1000; +} diff --git a/src/common/helper/helper.module.ts b/src/common/helper/helper.module.ts new file mode 100644 index 0000000..4e9231b --- /dev/null +++ b/src/common/helper/helper.module.ts @@ -0,0 +1,53 @@ +import { Global, Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { HelperArrayService } from './services/helper.array.service'; +import { HelperDateService } from './services/helper.date.service'; +import { HelperEncryptionService } from './services/helper.encryption.service'; +import { HelperHashService } from './services/helper.hash.service'; +import { HelperNumberService } from './services/helper.number.service'; +import { HelperStringService } from './services/helper.string.service'; +import { HelperFileService } from './services/helper.file.service'; +import { HelperGeoService } from './services/helper.geo.service'; + +@Global() +@Module({ + providers: [ + HelperArrayService, + HelperDateService, + HelperEncryptionService, + HelperHashService, + HelperNumberService, + HelperStringService, + HelperFileService, + HelperGeoService, + ], + exports: [ + HelperArrayService, + HelperDateService, + HelperEncryptionService, + HelperHashService, + HelperNumberService, + HelperStringService, + HelperFileService, + HelperGeoService, + ], + controllers: [], + imports: [ + JwtModule.registerAsync({ + inject: [ConfigService], + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + secret: configService.get( + 'helper.jwt.defaultSecretKey' + ), + signOptions: { + expiresIn: configService.get( + 'helper.jwt.defaultExpirationTime' + ), + }, + }), + }), + ], +}) +export class HelperModule {} diff --git a/src/common/helper/interfaces/helper.array-service.interface.ts b/src/common/helper/interfaces/helper.array-service.interface.ts new file mode 100644 index 0000000..e5f8641 --- /dev/null +++ b/src/common/helper/interfaces/helper.array-service.interface.ts @@ -0,0 +1,57 @@ +import { IHelperArrayRemove } from 'src/common/helper/interfaces/helper.interface'; + +export interface IHelperArrayService { + getLeftByIndex(array: T[], index: number): T; + + getRightByIndex(array: T[], index: number): T; + + getLeftByLength(array: T[], length: number): T[]; + + getRightByLength(array: T[], length: number): T[]; + + getLast(array: T[]): T; + + getFirst(array: T[]): T; + + getFirstIndexByValue(array: T[], value: T): number; + + getLastIndexByValue(array: T[], value: T): number; + + removeByValue(array: T[], value: T): IHelperArrayRemove; + + removeLeftByLength(array: T[], length: number): T[]; + + removeRightByLength(array: Array, length: number): T[]; + + joinToString(array: Array, delimiter: string): string; + + reverse(array: T[]): T[]; + + unique(array: T[]): T[]; + + shuffle(array: T[]): T[]; + + merge(a: T[], b: T[]): T[]; + + mergeUnique(a: T[], b: T[]): T[]; + + filterIncludeByValue(array: T[], value: T): T[]; + + filterNotIncludeByValue(array: T[], value: T): T[]; + + filterNotIncludeUniqueByArray(a: T[], b: T[]): T[]; + + filterIncludeUniqueByArray(a: T[], b: T[]): T[]; + + equals(a: T[], b: T[]): boolean; + + notEquals(a: T[], b: T[]): boolean; + + in(a: T[], b: T[]): boolean; + + notIn(a: T[], b: T[]): boolean; + + includes(a: T[], b: T): boolean; + + chunk(a: T[], size: number): T[][]; +} diff --git a/src/common/helper/interfaces/helper.date-service.interface.ts b/src/common/helper/interfaces/helper.date-service.interface.ts new file mode 100644 index 0000000..29973b4 --- /dev/null +++ b/src/common/helper/interfaces/helper.date-service.interface.ts @@ -0,0 +1,99 @@ +import { + IHelperDateExtractDate, + IHelperDateOptionsBackward, + IHelperDateOptionsCreate, + IHelperDateOptionsDiff, + IHelperDateOptionsFormat, + IHelperDateOptionsForward, + IHelperDateStartAndEnd, + IHelperDateStartAndEndDate, +} from 'src/common/helper/interfaces/helper.interface'; + +export interface IHelperDateService { + calculateAge(dateOfBirth: Date): number; + + diff( + dateOne: Date, + dateTwoMoreThanDateOne: Date, + options?: IHelperDateOptionsDiff + ): number; + + check(date: string | Date | number): boolean; + + checkTimestamp(timestamp: number): boolean; + + create( + date?: string | Date | number, + options?: IHelperDateOptionsCreate + ): Date; + + timestamp( + date?: string | Date | number, + options?: IHelperDateOptionsCreate + ): number; + + format(date: Date, options?: IHelperDateOptionsFormat): string; + + forwardInMilliseconds( + milliseconds: number, + options?: IHelperDateOptionsForward + ): Date; + + backwardInMilliseconds( + milliseconds: number, + options?: IHelperDateOptionsBackward + ): Date; + + forwardInSeconds( + seconds: number, + options?: IHelperDateOptionsForward + ): Date; + + backwardInSeconds( + seconds: number, + options?: IHelperDateOptionsBackward + ): Date; + + forwardInMinutes( + minutes: number, + options?: IHelperDateOptionsForward + ): Date; + + backwardInMinutes( + minutes: number, + options?: IHelperDateOptionsBackward + ): Date; + + forwardInHours(hours: number, options?: IHelperDateOptionsForward): Date; + + backwardInHours(hours: number, options?: IHelperDateOptionsBackward): Date; + + forwardInDays(days: number, options?: IHelperDateOptionsForward): Date; + + backwardInDays(days: number, options?: IHelperDateOptionsBackward): Date; + + forwardInMonths(months: number, options?: IHelperDateOptionsForward): Date; + + backwardInMonths( + months: number, + options?: IHelperDateOptionsBackward + ): Date; + + endOfMonth(date?: Date): Date; + + startOfMonth(date?: Date): Date; + + endOfYear(date?: Date): Date; + + startOfYear(date?: Date): Date; + + endOfDay(date?: Date): Date; + + startOfDay(date?: Date): Date; + + extractDate(date: string | Date | number): IHelperDateExtractDate; + + getStartAndEndDate( + options?: IHelperDateStartAndEnd + ): IHelperDateStartAndEndDate; +} diff --git a/src/common/helper/interfaces/helper.encryption-service.interface.ts b/src/common/helper/interfaces/helper.encryption-service.interface.ts new file mode 100644 index 0000000..4c18ee2 --- /dev/null +++ b/src/common/helper/interfaces/helper.encryption-service.interface.ts @@ -0,0 +1,33 @@ +import { + IHelperJwtOptions, + IHelperJwtVerifyOptions, +} from 'src/common/helper/interfaces/helper.interface'; + +export interface IHelperEncryptionService { + base64Encrypt(data: string): string; + + base64Decrypt(data: string): string; + + base64Compare(clientBasicToken: string, ourBasicToken: string): boolean; + + aes256Encrypt( + data: string | Record | Record[], + key: string, + iv: string + ): string; + + aes256Decrypt( + encrypted: string, + key: string, + iv: string + ): string | Record | Record[]; + + jwtEncrypt( + payload: Record, + options: IHelperJwtOptions + ): string; + + jwtDecrypt(token: string): Record; + + jwtVerify(token: string, options: IHelperJwtVerifyOptions): boolean; +} diff --git a/src/common/helper/interfaces/helper.file-service.interface.ts b/src/common/helper/interfaces/helper.file-service.interface.ts new file mode 100644 index 0000000..0479b01 --- /dev/null +++ b/src/common/helper/interfaces/helper.file-service.interface.ts @@ -0,0 +1,26 @@ +import { + IHelperFileWriteExcelOptions, + IHelperFileReadExcelOptions, + IHelperFileRows, + IHelperFileCreateExcelWorkbookOptions, +} from 'src/common/helper/interfaces/helper.interface'; +import { WorkBook } from 'xlsx'; + +export interface IHelperFileService { + createExcelWorkbook( + rows: IHelperFileRows[], + options?: IHelperFileCreateExcelWorkbookOptions + ): WorkBook; + + writeExcelToBuffer( + workbook: WorkBook, + options?: IHelperFileWriteExcelOptions + ): Buffer; + + readExcelFromBuffer( + file: Buffer, + options?: IHelperFileReadExcelOptions + ): IHelperFileRows[]; + + convertToBytes(megabytes: string): number; +} diff --git a/src/common/helper/interfaces/helper.geo-service.interface.ts b/src/common/helper/interfaces/helper.geo-service.interface.ts new file mode 100644 index 0000000..60f05b4 --- /dev/null +++ b/src/common/helper/interfaces/helper.geo-service.interface.ts @@ -0,0 +1,8 @@ +import { + IHelperGeoCurrent, + IHelperGeoRules, +} from 'src/common/helper/interfaces/helper.interface'; + +export interface IHelperGeoService { + inRadius(geoRule: IHelperGeoRules, geoCurrent: IHelperGeoCurrent): boolean; +} diff --git a/src/common/helper/interfaces/helper.hash-service.interface.ts b/src/common/helper/interfaces/helper.hash-service.interface.ts new file mode 100644 index 0000000..52d890d --- /dev/null +++ b/src/common/helper/interfaces/helper.hash-service.interface.ts @@ -0,0 +1,11 @@ +export interface IHelperHashService { + randomSalt(length: number): string; + + bcrypt(passwordString: string, salt: string): string; + + bcryptCompare(passwordString: string, passwordHashed: string): boolean; + + sha256(string: string): string; + + sha256Compare(hashOne: string, hashTwo: string): boolean; +} diff --git a/src/common/helper/interfaces/helper.interface.ts b/src/common/helper/interfaces/helper.interface.ts new file mode 100644 index 0000000..7c67376 --- /dev/null +++ b/src/common/helper/interfaces/helper.interface.ts @@ -0,0 +1,101 @@ +import { + ENUM_HELPER_DATE_DIFF, + ENUM_HELPER_DATE_FORMAT, + ENUM_HELPER_FILE_TYPE, +} from 'src/common/helper/constants/helper.enum.constant'; + +// Helper Array +export interface IHelperArrayRemove { + removed: T[]; + arrays: T[]; +} + +// Helper Encryption +export interface IHelperJwtVerifyOptions { + audience: string; + issuer: string; + subject: string; + secretKey: string; +} + +export interface IHelperJwtOptions extends IHelperJwtVerifyOptions { + expiredIn: number | string; + notBefore?: number | string; +} + +// Helper String +export interface IHelperStringRandomOptions { + upperCase?: boolean; + safe?: boolean; + prefix?: string; +} + +// Helper Geo +export interface IHelperGeoCurrent { + latitude: number; + longitude: number; +} + +export interface IHelperGeoRules extends IHelperGeoCurrent { + radiusInMeters: number; +} + +// Helper Date +export interface IHelperDateStartAndEnd { + month?: number; + year?: number; +} + +export interface IHelperDateStartAndEndDate { + startDate: Date; + endDate: Date; +} + +export interface IHelperDateExtractDate { + date: Date; + day: string; + month: string; + year: string; +} + +export interface IHelperDateOptionsDiff { + format?: ENUM_HELPER_DATE_DIFF; +} + +export interface IHelperDateOptionsCreate { + startOfDay?: boolean; +} + +export interface IHelperDateOptionsFormat { + format?: ENUM_HELPER_DATE_FORMAT | string; +} + +export interface IHelperDateOptionsForward { + fromDate?: Date; +} + +export type IHelperDateOptionsBackward = IHelperDateOptionsForward; + +export interface IHelperDateOptionsRoundDown { + hour: boolean; + minute: boolean; + second: boolean; +} + +// Helper File + +export type IHelperFileRows = Record; + +export interface IHelperFileWriteExcelOptions { + password?: string; + type?: ENUM_HELPER_FILE_TYPE; +} + +export interface IHelperFileCreateExcelWorkbookOptions { + sheetName?: string; +} + +export interface IHelperFileReadExcelOptions { + sheet?: string | number; + password?: string; +} diff --git a/src/common/helper/interfaces/helper.number-service.interface.ts b/src/common/helper/interfaces/helper.number-service.interface.ts new file mode 100644 index 0000000..7fe5a46 --- /dev/null +++ b/src/common/helper/interfaces/helper.number-service.interface.ts @@ -0,0 +1,11 @@ +export interface IHelperNumberService { + check(number: string): boolean; + + create(number: string): number; + + random(length: number): number; + + randomInRange(min: number, max: number): number; + + percent(value: number, total: number): number; +} diff --git a/src/common/helper/interfaces/helper.string-service.interface.ts b/src/common/helper/interfaces/helper.string-service.interface.ts new file mode 100644 index 0000000..b743edc --- /dev/null +++ b/src/common/helper/interfaces/helper.string-service.interface.ts @@ -0,0 +1,19 @@ +import { IHelperStringRandomOptions } from 'src/common/helper/interfaces/helper.interface'; + +export interface IHelperStringService { + checkEmail(email: string): boolean; + + randomReference(length: number, prefix?: string): string; + + random(length: number, options?: IHelperStringRandomOptions): string; + + censor(value: string): string; + + checkPasswordWeak(password: string, length?: number): boolean; + + checkPasswordMedium(password: string, length?: number): boolean; + + checkPasswordStrong(password: string, length?: number): boolean; + + checkSafeString(text: string): boolean; +} diff --git a/src/common/helper/services/helper.array.service.ts b/src/common/helper/services/helper.array.service.ts new file mode 100644 index 0000000..dd4b2bf --- /dev/null +++ b/src/common/helper/services/helper.array.service.ts @@ -0,0 +1,119 @@ +import { Injectable } from '@nestjs/common'; +import _ from 'lodash'; +import { IHelperArrayService } from 'src/common/helper/interfaces/helper.array-service.interface'; +import { IHelperArrayRemove } from 'src/common/helper/interfaces/helper.interface'; + +@Injectable() +export class HelperArrayService implements IHelperArrayService { + getLeftByIndex(array: T[], index: number): T { + return _.nth(array, index); + } + + getRightByIndex(array: T[], index: number): T { + return _.nth(array, -Math.abs(index)); + } + + getLeftByLength(array: T[], length: number): T[] { + return _.take(array, length); + } + + getRightByLength(array: T[], length: number): T[] { + return _.takeRight(array, length); + } + + getLast(array: T[]): T { + return _.last(array); + } + + getFirst(array: T[]): T { + return _.head(array); + } + + getFirstIndexByValue(array: T[], value: T): number { + return _.indexOf(array, value); + } + + getLastIndexByValue(array: T[], value: T): number { + return _.lastIndexOf(array, value); + } + + removeByValue(array: T[], value: T): IHelperArrayRemove { + const removed = _.remove(array, function (n) { + return n === value; + }); + + return { removed, arrays: array }; + } + + removeLeftByLength(array: T[], length: number): T[] { + return _.drop(array, length); + } + + removeRightByLength(array: Array, length: number): T[] { + return _.dropRight(array, length); + } + + joinToString(array: Array, delimiter: string): string { + return _.join(array, delimiter); + } + + reverse(array: T[]): T[] { + return _.reverse(array); + } + + unique(array: T[]): T[] { + return _.uniq(array); + } + + shuffle(array: T[]): T[] { + return _.shuffle(array); + } + + merge(a: T[], b: T[]): T[] { + return _.concat(a, b); + } + + mergeUnique(a: T[], b: T[]): T[] { + return _.union(a, b); + } + + filterIncludeByValue(array: T[], value: T): T[] { + return _.filter(array, (arr) => arr === value); + } + + filterNotIncludeByValue(array: T[], value: T): T[] { + return _.without(array, value); + } + + filterNotIncludeUniqueByArray(a: T[], b: T[]): T[] { + return _.xor(a, b); + } + + filterIncludeUniqueByArray(a: T[], b: T[]): T[] { + return _.intersection(a, b); + } + + equals(a: T[], b: T[]): boolean { + return _.isEqual(a, b); + } + + notEquals(a: T[], b: T[]): boolean { + return !_.isEqual(a, b); + } + + in(a: T[], b: T[]): boolean { + return _.intersection(a, b).length > 0; + } + + notIn(a: T[], b: T[]): boolean { + return _.intersection(a, b).length == 0; + } + + includes(a: T[], b: T): boolean { + return _.includes(a, b); + } + + chunk(a: T[], size: number): T[][] { + return _.chunk(a, size); + } +} diff --git a/src/common/helper/services/helper.date.service.ts b/src/common/helper/services/helper.date.service.ts new file mode 100644 index 0000000..6a74b9f --- /dev/null +++ b/src/common/helper/services/helper.date.service.ts @@ -0,0 +1,244 @@ +import { Injectable } from '@nestjs/common'; +import moment from 'moment'; +import { + ENUM_HELPER_DATE_DIFF, + ENUM_HELPER_DATE_FORMAT, +} from 'src/common/helper/constants/helper.enum.constant'; +import { IHelperDateService } from 'src/common/helper/interfaces/helper.date-service.interface'; +import { + IHelperDateExtractDate, + IHelperDateOptionsBackward, + IHelperDateOptionsCreate, + IHelperDateOptionsDiff, + IHelperDateOptionsFormat, + IHelperDateOptionsForward, + IHelperDateOptionsRoundDown, + IHelperDateStartAndEnd, + IHelperDateStartAndEndDate, +} from 'src/common/helper/interfaces/helper.interface'; + +@Injectable() +export class HelperDateService implements IHelperDateService { + calculateAge(dateOfBirth: Date): number { + return moment().diff(dateOfBirth, 'years'); + } + + diff( + dateOne: Date, + dateTwoMoreThanDateOne: Date, + options?: IHelperDateOptionsDiff + ): number { + const mDateOne = moment(dateOne); + const mDateTwo = moment(dateTwoMoreThanDateOne); + const diff = moment.duration(mDateTwo.diff(mDateOne)); + + if (options?.format === ENUM_HELPER_DATE_DIFF.MILIS) { + return diff.asMilliseconds(); + } else if (options?.format === ENUM_HELPER_DATE_DIFF.SECONDS) { + return diff.asSeconds(); + } else if (options?.format === ENUM_HELPER_DATE_DIFF.HOURS) { + return diff.asHours(); + } else if (options?.format === ENUM_HELPER_DATE_DIFF.MINUTES) { + return diff.asMinutes(); + } else { + return diff.asDays(); + } + } + + check(date: string | Date | number): boolean { + return moment(date, true).isValid(); + } + + checkTimestamp(timestamp: number): boolean { + return moment(timestamp, true).isValid(); + } + + create( + date?: string | number | Date, + options?: IHelperDateOptionsCreate + ): Date { + const mDate = moment(date ?? undefined); + + if (options?.startOfDay) { + mDate.startOf('day'); + } + + return mDate.toDate(); + } + + timestamp( + date?: string | number | Date, + options?: IHelperDateOptionsCreate + ): number { + const mDate = moment(date ?? undefined); + + if (options?.startOfDay) { + mDate.startOf('day'); + } + + return mDate.valueOf(); + } + + format(date: Date, options?: IHelperDateOptionsFormat): string { + return moment(date).format( + options?.format ?? ENUM_HELPER_DATE_FORMAT.DATE + ); + } + + forwardInMilliseconds( + milliseconds: number, + options?: IHelperDateOptionsForward + ): Date { + return moment(options?.fromDate).add(milliseconds, 'ms').toDate(); + } + + backwardInMilliseconds( + milliseconds: number, + options?: IHelperDateOptionsBackward + ): Date { + return moment(options?.fromDate).subtract(milliseconds, 'ms').toDate(); + } + + forwardInSeconds( + seconds: number, + options?: IHelperDateOptionsForward + ): Date { + return moment(options?.fromDate).add(seconds, 's').toDate(); + } + + backwardInSeconds( + seconds: number, + options?: IHelperDateOptionsBackward + ): Date { + return moment(options?.fromDate).subtract(seconds, 's').toDate(); + } + + forwardInMinutes( + minutes: number, + options?: IHelperDateOptionsForward + ): Date { + return moment(options?.fromDate).add(minutes, 'm').toDate(); + } + + backwardInMinutes( + minutes: number, + options?: IHelperDateOptionsBackward + ): Date { + return moment(options?.fromDate).subtract(minutes, 'm').toDate(); + } + + forwardInHours(hours: number, options?: IHelperDateOptionsForward): Date { + return moment(options?.fromDate).add(hours, 'h').toDate(); + } + + backwardInHours(hours: number, options?: IHelperDateOptionsBackward): Date { + return moment(options?.fromDate).subtract(hours, 'h').toDate(); + } + + forwardInDays(days: number, options?: IHelperDateOptionsForward): Date { + return moment(options?.fromDate).add(days, 'd').toDate(); + } + + backwardInDays(days: number, options?: IHelperDateOptionsBackward): Date { + return moment(options?.fromDate).subtract(days, 'd').toDate(); + } + + forwardInMonths(months: number, options?: IHelperDateOptionsForward): Date { + return moment(options?.fromDate).add(months, 'M').toDate(); + } + + backwardInMonths( + months: number, + options?: IHelperDateOptionsBackward + ): Date { + return moment(options?.fromDate).subtract(months, 'M').toDate(); + } + + endOfMonth(date?: Date): Date { + return moment(date).endOf('month').toDate(); + } + + startOfMonth(date?: Date): Date { + return moment(date).startOf('month').toDate(); + } + + endOfYear(date?: Date): Date { + return moment(date).endOf('year').toDate(); + } + + startOfYear(date?: Date): Date { + return moment(date).startOf('year').toDate(); + } + + endOfDay(date?: Date): Date { + return moment(date).endOf('day').toDate(); + } + + startOfDay(date?: Date): Date { + return moment(date).startOf('day').toDate(); + } + + extractDate(date: string | Date | number): IHelperDateExtractDate { + const newDate = this.create(date); + const day: string = this.format(newDate, { + format: ENUM_HELPER_DATE_FORMAT.ONLY_DATE, + }); + const month: string = this.format(newDate, { + format: ENUM_HELPER_DATE_FORMAT.ONLY_MONTH, + }); + const year: string = this.format(newDate, { + format: ENUM_HELPER_DATE_FORMAT.ONLY_YEAR, + }); + + return { + date: newDate, + day, + month, + year, + }; + } + + roundDown(date: Date, options?: IHelperDateOptionsRoundDown): Date { + const mDate = moment(date).set({ millisecond: 0 }); + + if (options?.hour) { + mDate.set({ hour: 0 }); + } + + if (options?.minute) { + mDate.set({ minute: 0 }); + } + + if (options?.second) { + mDate.set({ second: 0 }); + } + + return mDate.toDate(); + } + + getStartAndEndDate( + options?: IHelperDateStartAndEnd + ): IHelperDateStartAndEndDate { + const today = moment(); + const todayMonth = today.format(ENUM_HELPER_DATE_FORMAT.ONLY_MONTH); + const todayYear = today.format(ENUM_HELPER_DATE_FORMAT.ONLY_YEAR); + // set month and year + const year = options?.year ?? todayYear; + const month = options?.month ?? todayMonth; + + const date = moment(`${year}-${month}-02`, 'YYYY-MM-DD'); + let startDate: Date = date.startOf('year').toDate(); + let endDate: Date = date.endOf('year').toDate(); + + if (options?.month) { + const date = moment(`${year}-${month}-02`, 'YYYY-MM-DD'); + startDate = date.startOf('month').toDate(); + endDate = date.endOf('month').toDate(); + } + + return { + startDate, + endDate, + }; + } +} diff --git a/src/common/helper/services/helper.encryption.service.ts b/src/common/helper/services/helper.encryption.service.ts new file mode 100644 index 0000000..347997a --- /dev/null +++ b/src/common/helper/services/helper.encryption.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { AES, enc, mode, pad } from 'crypto-js'; +import { IHelperEncryptionService } from 'src/common/helper/interfaces/helper.encryption-service.interface'; +import { + IHelperJwtOptions, + IHelperJwtVerifyOptions, +} from 'src/common/helper/interfaces/helper.interface'; + +@Injectable() +export class HelperEncryptionService implements IHelperEncryptionService { + constructor(private readonly jwtService: JwtService) {} + + base64Encrypt(data: string): string { + const buff: Buffer = Buffer.from(data, 'utf8'); + return buff.toString('base64'); + } + + base64Decrypt(data: string): string { + const buff: Buffer = Buffer.from(data, 'base64'); + return buff.toString('utf8'); + } + + base64Compare(clientBasicToken: string, ourBasicToken: string): boolean { + return ourBasicToken === clientBasicToken; + } + + aes256Encrypt( + data: string | Record | Record[], + key: string, + iv: string + ): string { + const cIv = enc.Utf8.parse(iv); + const cipher = AES.encrypt(JSON.stringify(data), key, { + mode: mode.CBC, + padding: pad.Pkcs7, + iv: cIv, + }); + + return cipher.toString(); + } + + aes256Decrypt( + encrypted: string, + key: string, + iv: string + ): string | Record | Record[] { + const cIv = enc.Utf8.parse(iv); + const cipher = AES.decrypt(encrypted, key, { + mode: mode.CBC, + padding: pad.Pkcs7, + iv: cIv, + }); + + return JSON.parse(cipher.toString(enc.Utf8)); + } + + jwtEncrypt( + payload: Record, + options: IHelperJwtOptions + ): string { + return this.jwtService.sign(payload, { + secret: options.secretKey, + expiresIn: options.expiredIn, + notBefore: options.notBefore ?? 0, + audience: options.audience, + issuer: options.issuer, + subject: options.subject, + }); + } + + jwtDecrypt(token: string): Record { + return this.jwtService.decode(token) as Record; + } + + jwtVerify(token: string, options: IHelperJwtVerifyOptions): boolean { + try { + this.jwtService.verify(token, { + secret: options.secretKey, + audience: options.audience, + issuer: options.issuer, + subject: options.subject, + }); + return true; + } catch (err: unknown) { + return false; + } + } +} diff --git a/src/common/helper/services/helper.file.service.ts b/src/common/helper/services/helper.file.service.ts new file mode 100644 index 0000000..03244a3 --- /dev/null +++ b/src/common/helper/services/helper.file.service.ts @@ -0,0 +1,76 @@ +import { Injectable } from '@nestjs/common'; +import bytes from 'bytes'; +import { ENUM_HELPER_FILE_TYPE } from 'src/common/helper/constants/helper.enum.constant'; +import { IHelperFileService } from 'src/common/helper/interfaces/helper.file-service.interface'; +import { + IHelperFileWriteExcelOptions, + IHelperFileReadExcelOptions, + IHelperFileRows, + IHelperFileCreateExcelWorkbookOptions, +} from 'src/common/helper/interfaces/helper.interface'; +import { utils, write, read, WorkBook } from 'xlsx'; + +@Injectable() +export class HelperFileService implements IHelperFileService { + createExcelWorkbook( + rows: IHelperFileRows[], + options?: IHelperFileCreateExcelWorkbookOptions + ): WorkBook { + // headers + const headers = rows.length > 0 ? Object.keys(rows[0]) : []; + + // worksheet + const worksheet = utils.json_to_sheet(rows); + + // workbook + const workbook = utils.book_new(); + + utils.sheet_add_aoa(worksheet, [headers], { origin: 'A1' }); + utils.book_append_sheet( + workbook, + worksheet, + options?.sheetName ?? 'Sheet 1' + ); + + return workbook; + } + + writeExcelToBuffer( + workbook: WorkBook, + options?: IHelperFileWriteExcelOptions + ): Buffer { + // create buffer + const buff: Buffer = write(workbook, { + type: 'buffer', + bookType: options?.type ?? ENUM_HELPER_FILE_TYPE.CSV, + password: options?.password, + }); + + return buff; + } + + readExcelFromBuffer( + file: Buffer, + options?: IHelperFileReadExcelOptions + ): IHelperFileRows[] { + // workbook + const workbook = read(file, { + type: 'buffer', + password: options?.password, + sheets: options?.sheet, + }); + + // worksheet + const worksheetName = workbook.SheetNames; + const worksheet = workbook.Sheets[worksheetName[0]]; + + // rows + const rows: IHelperFileRows[] = utils.sheet_to_json(worksheet); + + return rows; + } + + convertToBytes(megabytes: string): number { + return bytes(megabytes); + } +} diff --git a/src/common/helper/services/helper.geo.service.ts b/src/common/helper/services/helper.geo.service.ts new file mode 100644 index 0000000..deaad14 --- /dev/null +++ b/src/common/helper/services/helper.geo.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { isPointWithinRadius } from 'geolib'; +import { IHelperGeoService } from 'src/common/helper/interfaces/helper.geo-service.interface'; +import { + IHelperGeoCurrent, + IHelperGeoRules, +} from 'src/common/helper/interfaces/helper.interface'; + +@Injectable() +export class HelperGeoService implements IHelperGeoService { + inRadius(geoRule: IHelperGeoRules, geoCurrent: IHelperGeoCurrent): boolean { + return isPointWithinRadius( + { latitude: geoRule.latitude, longitude: geoRule.longitude }, + geoCurrent, + geoRule.radiusInMeters + ); + } +} diff --git a/src/common/helper/services/helper.hash.service.ts b/src/common/helper/services/helper.hash.service.ts new file mode 100644 index 0000000..c7c9987 --- /dev/null +++ b/src/common/helper/services/helper.hash.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { compareSync, genSaltSync, hashSync } from 'bcryptjs'; +import { SHA256, enc } from 'crypto-js'; +import { IHelperHashService } from 'src/common/helper/interfaces/helper.hash-service.interface'; + +@Injectable() +export class HelperHashService implements IHelperHashService { + randomSalt(length: number): string { + return genSaltSync(length); + } + + bcrypt(passwordString: string, salt: string): string { + return hashSync(passwordString, salt); + } + + bcryptCompare(passwordString: string, passwordHashed: string): boolean { + return compareSync(passwordString, passwordHashed); + } + + sha256(string: string): string { + return SHA256(string).toString(enc.Hex); + } + + sha256Compare(hashOne: string, hashTwo: string): boolean { + return hashOne === hashTwo; + } +} diff --git a/src/common/helper/services/helper.number.service.ts b/src/common/helper/services/helper.number.service.ts new file mode 100644 index 0000000..f34224a --- /dev/null +++ b/src/common/helper/services/helper.number.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { faker } from '@faker-js/faker'; +import { IHelperNumberService } from 'src/common/helper/interfaces/helper.number-service.interface'; + +@Injectable() +export class HelperNumberService implements IHelperNumberService { + check(number: string): boolean { + const regex = /^-?\d+$/; + return regex.test(number); + } + + create(number: string): number { + return Number(number); + } + + random(length: number): number { + const min: number = Number.parseInt(`1`.padEnd(length, '0')); + const max: number = Number.parseInt(`9`.padEnd(length, '9')); + return this.randomInRange(min, max); + } + + randomInRange(min: number, max: number): number { + return faker.datatype.number({ min, max }); + } + + percent(value: number, total: number): number { + let tValue = value / total; + if (Number.isNaN(tValue) || !Number.isFinite(tValue)) { + tValue = 0; + } + return Number.parseFloat((tValue * 100).toFixed(2)); + } +} diff --git a/src/common/helper/services/helper.string.service.ts b/src/common/helper/services/helper.string.service.ts new file mode 100644 index 0000000..caeb372 --- /dev/null +++ b/src/common/helper/services/helper.string.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@nestjs/common'; +import { faker } from '@faker-js/faker'; +import { HelperDateService } from './helper.date.service'; +import { IHelperStringRandomOptions } from 'src/common/helper/interfaces/helper.interface'; +import { IHelperStringService } from 'src/common/helper/interfaces/helper.string-service.interface'; + +@Injectable() +export class HelperStringService implements IHelperStringService { + constructor(private readonly helperDateService: HelperDateService) {} + + checkEmail(email: string): boolean { + const regex = /\S+@\S+\.\S+/; + return regex.test(email); + } + + randomReference(length: number, prefix?: string): string { + const timestamp = `${this.helperDateService.timestamp()}`; + const randomString: string = this.random(length, { + safe: true, + upperCase: true, + }); + + return prefix + ? `${prefix}-${timestamp}${randomString}` + : `${timestamp}${randomString}`; + } + + random(length: number, options?: IHelperStringRandomOptions): string { + const rString = options?.safe + ? faker.internet.password(length, true, /[A-Z]/, options?.prefix) + : faker.internet.password(length, false, /\w/, options?.prefix); + + return options?.upperCase ? rString.toUpperCase() : rString; + } + + censor(value: string): string { + const length = value.length; + if (length === 1) { + return value; + } + + const end = length > 4 ? length - 4 : 1; + const censorString = '*'.repeat(end > 10 ? 10 : end); + const visibleString = value.substring(end, length); + return `${censorString}${visibleString}`; + } + + checkPasswordWeak(password: string, length?: number): boolean { + const regex = new RegExp( + `^(?=.*?[A-Z])(?=.*?[a-z]).{${length ?? 8},}$` + ); + + return regex.test(password); + } + + checkPasswordMedium(password: string, length?: number): boolean { + const regex = new RegExp( + `^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{${length ?? 8},}$` + ); + + return regex.test(password); + } + + checkPasswordStrong(password: string, length?: number): boolean { + const regex = new RegExp( + `^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{${ + length ?? 8 + },}$` + ); + + return regex.test(password); + } + + checkSafeString(text: string): boolean { + const regex = new RegExp('^[A-Za-z0-9_-]+$'); + return regex.test(text); + } +} diff --git a/src/common/logger/constants/logger.constant.ts b/src/common/logger/constants/logger.constant.ts new file mode 100644 index 0000000..7a9a388 --- /dev/null +++ b/src/common/logger/constants/logger.constant.ts @@ -0,0 +1,2 @@ +export const LOGGER_ACTION_META_KEY = 'LoggerActionMetaKey'; +export const LOGGER_OPTIONS_META_KEY = 'LoggerOptionsMetaKey'; diff --git a/src/common/logger/constants/logger.enum.constant.ts b/src/common/logger/constants/logger.enum.constant.ts new file mode 100644 index 0000000..6ff677a --- /dev/null +++ b/src/common/logger/constants/logger.enum.constant.ts @@ -0,0 +1,11 @@ +export enum ENUM_LOGGER_LEVEL { + DEBUG = 'DEBUG', + INFO = 'INFO', + WARM = 'WARM', + FATAL = 'FATAL', +} + +export enum ENUM_LOGGER_ACTION { + LOGIN = 'LOGIN', + TEST = 'TEST', +} diff --git a/src/common/logger/decorators/logger.decorator.ts b/src/common/logger/decorators/logger.decorator.ts new file mode 100644 index 0000000..4fc569b --- /dev/null +++ b/src/common/logger/decorators/logger.decorator.ts @@ -0,0 +1,19 @@ +import { applyDecorators, SetMetadata, UseInterceptors } from '@nestjs/common'; +import { + LOGGER_ACTION_META_KEY, + LOGGER_OPTIONS_META_KEY, +} from 'src/common/logger/constants/logger.constant'; +import { ENUM_LOGGER_ACTION } from 'src/common/logger/constants/logger.enum.constant'; +import { LoggerInterceptor } from 'src/common/logger/interceptors/logger.interceptor'; +import { ILoggerOptions } from 'src/common/logger/interfaces/logger.interface'; + +export function Logger( + action: ENUM_LOGGER_ACTION, + options?: ILoggerOptions +): MethodDecorator { + return applyDecorators( + UseInterceptors(LoggerInterceptor), + SetMetadata(LOGGER_ACTION_META_KEY, action), + SetMetadata(LOGGER_OPTIONS_META_KEY, options ?? {}) + ); +} diff --git a/src/common/logger/dtos/logger.create.dto.ts b/src/common/logger/dtos/logger.create.dto.ts new file mode 100644 index 0000000..ae42558 --- /dev/null +++ b/src/common/logger/dtos/logger.create.dto.ts @@ -0,0 +1,26 @@ +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { + ENUM_LOGGER_ACTION, + ENUM_LOGGER_LEVEL, +} from 'src/common/logger/constants/logger.enum.constant'; +import { ENUM_REQUEST_METHOD } from 'src/common/request/constants/request.enum.constant'; + +export class LoggerCreateDto { + action: ENUM_LOGGER_ACTION; + description: string; + apiKey?: string; + user?: string; + requestId?: string; + method: ENUM_REQUEST_METHOD; + path: string; + role?: string; + accessFor?: ENUM_AUTH_ACCESS_FOR; + tags?: string[]; + params?: Record; + bodies?: Record; + statusCode?: number; +} + +export class LoggerCreateRawDto extends LoggerCreateDto { + level: ENUM_LOGGER_LEVEL; +} diff --git a/src/common/logger/interceptors/logger.interceptor.ts b/src/common/logger/interceptors/logger.interceptor.ts new file mode 100644 index 0000000..7571dd9 --- /dev/null +++ b/src/common/logger/interceptors/logger.interceptor.ts @@ -0,0 +1,91 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable, tap } from 'rxjs'; +import { Response } from 'express'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { Reflector } from '@nestjs/core'; +import { ENUM_REQUEST_METHOD } from 'src/common/request/constants/request.enum.constant'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { LoggerService } from 'src/common/logger/services/logger.service'; +import { + ENUM_LOGGER_ACTION, + ENUM_LOGGER_LEVEL, +} from 'src/common/logger/constants/logger.enum.constant'; +import { + LOGGER_ACTION_META_KEY, + LOGGER_OPTIONS_META_KEY, +} from 'src/common/logger/constants/logger.constant'; +import { ILoggerOptions } from 'src/common/logger/interfaces/logger.interface'; + +@Injectable() +export class LoggerInterceptor implements NestInterceptor { + constructor( + private readonly reflector: Reflector, + private readonly loggerService: LoggerService + ) {} + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise | string>> { + if (context.getType() === 'http') { + const ctx: HttpArgumentsHost = context.switchToHttp(); + const { + apiKey, + method, + originalUrl, + user, + __id, + body, + params, + path, + } = ctx.getRequest(); + const responseExpress = ctx.getResponse(); + + return next.handle().pipe( + tap(async (response: Promise>) => { + const responseData: Record = await response; + const responseStatus: number = responseExpress.statusCode; + const statusCode = + responseData?.statusCode ?? responseStatus; + + const loggerAction: ENUM_LOGGER_ACTION = + this.reflector.get( + LOGGER_ACTION_META_KEY, + context.getHandler() + ); + const loggerOptions: ILoggerOptions = + this.reflector.get( + LOGGER_OPTIONS_META_KEY, + context.getHandler() + ); + + await this.loggerService.raw({ + level: loggerOptions?.level ?? ENUM_LOGGER_LEVEL.INFO, + action: loggerAction, + description: + loggerOptions?.description ?? + `Request ${method} called, url ${originalUrl}, and action ${loggerAction}`, + apiKey: apiKey?._id, + user: user?._id, + requestId: __id, + method: method as ENUM_REQUEST_METHOD, + role: user?.role, + accessFor: user?.accessFor, + params, + bodies: body, + path, + statusCode, + tags: loggerOptions?.tags ?? [], + }); + }) + ); + } + + return next.handle(); + } +} diff --git a/src/common/logger/interfaces/logger.interface.ts b/src/common/logger/interfaces/logger.interface.ts new file mode 100644 index 0000000..d2fc900 --- /dev/null +++ b/src/common/logger/interfaces/logger.interface.ts @@ -0,0 +1,7 @@ +import { ENUM_LOGGER_LEVEL } from 'src/common/logger/constants/logger.enum.constant'; + +export interface ILoggerOptions { + description?: string; + tags?: string[]; + level?: ENUM_LOGGER_LEVEL; +} diff --git a/src/common/logger/interfaces/logger.service.interface.ts b/src/common/logger/interfaces/logger.service.interface.ts new file mode 100644 index 0000000..68711e4 --- /dev/null +++ b/src/common/logger/interfaces/logger.service.interface.ts @@ -0,0 +1,17 @@ +import { + LoggerCreateDto, + LoggerCreateRawDto, +} from 'src/common/logger/dtos/logger.create.dto'; +import { LoggerDoc } from 'src/common/logger/repository/entities/logger.entity'; + +export interface ILoggerService { + info(data: LoggerCreateDto): Promise; + + debug(data: LoggerCreateDto): Promise; + + warn(data: LoggerCreateDto): Promise; + + fatal(data: LoggerCreateDto): Promise; + + raw(data: LoggerCreateRawDto): Promise; +} diff --git a/src/common/logger/logger.module.ts b/src/common/logger/logger.module.ts new file mode 100644 index 0000000..c834b22 --- /dev/null +++ b/src/common/logger/logger.module.ts @@ -0,0 +1,11 @@ +import { Global, Module } from '@nestjs/common'; +import { LoggerRepositoryModule } from 'src/common/logger/repository/logger.repository.module'; +import { LoggerService } from './services/logger.service'; + +@Global() +@Module({ + providers: [LoggerService], + exports: [LoggerService], + imports: [LoggerRepositoryModule], +}) +export class LoggerModule {} diff --git a/src/common/logger/repository/entities/logger.entity.ts b/src/common/logger/repository/entities/logger.entity.ts new file mode 100644 index 0000000..89ebd79 --- /dev/null +++ b/src/common/logger/repository/entities/logger.entity.ts @@ -0,0 +1,117 @@ +import { Prop, SchemaFactory } from '@nestjs/mongoose'; +import { ApiKeyEntity } from 'src/common/api-key/repository/entities/api-key.entity'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntity } from 'src/common/database/decorators/database.decorator'; +import { + ENUM_LOGGER_ACTION, + ENUM_LOGGER_LEVEL, +} from 'src/common/logger/constants/logger.enum.constant'; +import { ENUM_REQUEST_METHOD } from 'src/common/request/constants/request.enum.constant'; +import { Document } from 'mongoose'; + +export const LoggerDatabaseName = 'loggers'; + +@DatabaseEntity({ collection: LoggerDatabaseName }) +export class LoggerEntity extends DatabaseMongoUUIDEntityAbstract { + @Prop({ + required: true, + enum: ENUM_LOGGER_LEVEL, + type: String, + }) + level: string; + + @Prop({ + required: true, + enum: ENUM_LOGGER_ACTION, + type: String, + }) + action: string; + + @Prop({ + required: true, + enum: ENUM_REQUEST_METHOD, + type: String, + }) + method: string; + + @Prop({ + required: false, + type: String, + }) + requestId?: string; + + @Prop({ + required: false, + type: String, + }) + user?: string; + + @Prop({ + required: false, + type: String, + }) + role?: string; + + @Prop({ + required: false, + ref: ApiKeyEntity.name, + type: String, + }) + apiKey?: string; + + @Prop({ + required: true, + default: true, + type: Boolean, + }) + anonymous: boolean; + + @Prop({ + required: false, + enum: ENUM_AUTH_ACCESS_FOR, + type: String, + }) + accessFor?: ENUM_AUTH_ACCESS_FOR; + + @Prop({ + required: true, + type: String, + }) + description: string; + + @Prop({ + required: false, + type: Object, + }) + params?: Record; + + @Prop({ + required: false, + type: Object, + }) + bodies?: Record; + + @Prop({ + required: false, + type: Number, + }) + statusCode?: number; + + @Prop({ + required: false, + type: String, + }) + path?: string; + + @Prop({ + required: false, + default: [], + type: Array, + }) + tags: string[]; +} + +export const LoggerSchema = SchemaFactory.createForClass(LoggerEntity); + +export type LoggerDoc = LoggerEntity & Document; diff --git a/src/common/logger/repository/logger.repository.module.ts b/src/common/logger/repository/logger.repository.module.ts new file mode 100644 index 0000000..890b413 --- /dev/null +++ b/src/common/logger/repository/logger.repository.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { + LoggerEntity, + LoggerSchema, +} from 'src/common/logger/repository/entities/logger.entity'; +import { LoggerRepository } from 'src/common/logger/repository/repositories/logger.repository'; + +@Module({ + providers: [LoggerRepository], + exports: [LoggerRepository], + controllers: [], + imports: [ + MongooseModule.forFeature( + [ + { + name: LoggerEntity.name, + schema: LoggerSchema, + }, + ], + DATABASE_CONNECTION_NAME + ), + ], +}) +export class LoggerRepositoryModule {} diff --git a/src/common/logger/repository/repositories/logger.repository.ts b/src/common/logger/repository/repositories/logger.repository.ts new file mode 100644 index 0000000..adb1497 --- /dev/null +++ b/src/common/logger/repository/repositories/logger.repository.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { Model } from 'mongoose'; +import { ApiKeyEntity } from 'src/common/api-key/repository/entities/api-key.entity'; +import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; +import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; +import { + LoggerDoc, + LoggerEntity, +} from 'src/common/logger/repository/entities/logger.entity'; + +@Injectable() +export class LoggerRepository extends DatabaseMongoUUIDRepositoryAbstract< + LoggerEntity, + LoggerDoc +> { + constructor( + @DatabaseModel(LoggerEntity.name) + private readonly LoggerDoc: Model + ) { + super(LoggerDoc, { + path: 'apiKey', + match: '_id', + model: ApiKeyEntity.name, + }); + } +} diff --git a/src/common/logger/services/logger.service.ts b/src/common/logger/services/logger.service.ts new file mode 100644 index 0000000..2379a8b --- /dev/null +++ b/src/common/logger/services/logger.service.ts @@ -0,0 +1,193 @@ +import { Injectable } from '@nestjs/common'; +import { ENUM_LOGGER_LEVEL } from 'src/common/logger/constants/logger.enum.constant'; +import { + LoggerCreateDto, + LoggerCreateRawDto, +} from 'src/common/logger/dtos/logger.create.dto'; +import { ILoggerService } from 'src/common/logger/interfaces/logger.service.interface'; +import { + LoggerDoc, + LoggerEntity, +} from 'src/common/logger/repository/entities/logger.entity'; +import { LoggerRepository } from 'src/common/logger/repository/repositories/logger.repository'; + +@Injectable() +export class LoggerService implements ILoggerService { + constructor(private readonly loggerRepository: LoggerRepository) {} + + async info({ + action, + description, + apiKey, + user, + method, + requestId, + role, + accessFor, + params, + bodies, + path, + statusCode, + tags, + }: LoggerCreateDto): Promise { + const create: LoggerEntity = new LoggerEntity(); + create.level = ENUM_LOGGER_LEVEL.INFO; + create.user = user; + create.apiKey = apiKey; + create.anonymous = !user; + create.action = action; + create.description = description; + create.method = method; + create.requestId = requestId; + create.role = role; + create.accessFor = accessFor; + create.params = params; + create.bodies = bodies; + create.path = path; + create.statusCode = statusCode; + create.tags = tags; + + return this.loggerRepository.create(create); + } + + async debug({ + action, + description, + apiKey, + user, + method, + requestId, + role, + accessFor, + params, + bodies, + path, + statusCode, + tags, + }: LoggerCreateDto): Promise { + const create: LoggerEntity = new LoggerEntity(); + create.level = ENUM_LOGGER_LEVEL.DEBUG; + create.user = user; + create.apiKey = apiKey; + create.anonymous = !user; + create.action = action; + create.description = description; + create.method = method; + create.requestId = requestId; + create.role = role; + create.accessFor = accessFor; + create.params = params; + create.bodies = bodies; + create.path = path; + create.statusCode = statusCode; + create.tags = tags; + + return this.loggerRepository.create(create); + } + + async warn({ + action, + description, + apiKey, + user, + method, + requestId, + role, + accessFor, + params, + bodies, + path, + statusCode, + tags, + }: LoggerCreateDto): Promise { + const create: LoggerEntity = new LoggerEntity(); + create.level = ENUM_LOGGER_LEVEL.WARM; + create.user = user; + create.apiKey = apiKey; + create.anonymous = !user; + create.action = action; + create.description = description; + create.method = method; + create.requestId = requestId; + create.role = role; + create.accessFor = accessFor; + create.params = params; + create.bodies = bodies; + create.path = path; + create.statusCode = statusCode; + create.tags = tags; + + return this.loggerRepository.create(create); + } + + async fatal({ + action, + description, + apiKey, + user, + method, + requestId, + role, + accessFor, + params, + bodies, + path, + statusCode, + tags, + }: LoggerCreateDto): Promise { + const create: LoggerEntity = new LoggerEntity(); + create.level = ENUM_LOGGER_LEVEL.FATAL; + create.user = user; + create.apiKey = apiKey; + create.anonymous = !user; + create.action = action; + create.description = description; + create.method = method; + create.requestId = requestId; + create.role = role; + create.accessFor = accessFor; + create.params = params; + create.bodies = bodies; + create.path = path; + create.statusCode = statusCode; + create.tags = tags; + + return this.loggerRepository.create(create); + } + + async raw({ + level, + action, + description, + apiKey, + user, + method, + requestId, + role, + accessFor, + params, + bodies, + path, + statusCode, + tags, + }: LoggerCreateRawDto): Promise { + const create: LoggerEntity = new LoggerEntity(); + create.level = level; + create.user = user; + create.apiKey = apiKey; + create.anonymous = !user; + create.action = action; + create.description = description; + create.method = method; + create.requestId = requestId; + create.role = role; + create.accessFor = accessFor; + create.params = params; + create.bodies = bodies; + create.path = path; + create.statusCode = statusCode; + create.tags = tags; + + return this.loggerRepository.create(create); + } +} diff --git a/src/common/message/constants/message.enum.constant.ts b/src/common/message/constants/message.enum.constant.ts new file mode 100644 index 0000000..f4e18bc --- /dev/null +++ b/src/common/message/constants/message.enum.constant.ts @@ -0,0 +1,4 @@ +export enum ENUM_MESSAGE_LANGUAGE { + EN = 'en', + ID = 'id', +} diff --git a/src/common/message/controllers/message.controller.ts b/src/common/message/controllers/message.controller.ts new file mode 100644 index 0000000..96032cd --- /dev/null +++ b/src/common/message/controllers/message.controller.ts @@ -0,0 +1,30 @@ +import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { MessageEnumLanguageDoc } from 'src/common/message/docs/message.enum.doc'; +import { MessageLanguageSerialization } from 'src/common/message/serializations/message.language.serialization'; +import { MessageService } from 'src/common/message/services/message.service'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; + +@ApiTags('message') +@Controller({ + version: VERSION_NEUTRAL, + path: '/message', +}) +export class MessageController { + constructor(private readonly messageService: MessageService) {} + + @MessageEnumLanguageDoc() + @Response('message.languages', { + serialization: MessageLanguageSerialization, + }) + @Get('/languages') + async languages(): Promise { + const languages: string[] = + await this.messageService.getAvailableLanguages(); + + return { + data: { languages }, + }; + } +} diff --git a/src/common/message/docs/message.enum.doc.ts b/src/common/message/docs/message.enum.doc.ts new file mode 100644 index 0000000..e5742bf --- /dev/null +++ b/src/common/message/docs/message.enum.doc.ts @@ -0,0 +1,11 @@ +import { applyDecorators } from '@nestjs/common'; +import { Doc } from 'src/common/doc/decorators/doc.decorator'; +import { MessageLanguageSerialization } from 'src/common/message/serializations/message.language.serialization'; + +export function MessageEnumLanguageDoc(): MethodDecorator { + return applyDecorators( + Doc('message.languages', { + response: { serialization: MessageLanguageSerialization }, + }) + ); +} diff --git a/src/common/message/interfaces/message.interface.ts b/src/common/message/interfaces/message.interface.ts new file mode 100644 index 0000000..210b335 --- /dev/null +++ b/src/common/message/interfaces/message.interface.ts @@ -0,0 +1,9 @@ +export type IMessage = Record; + +export type IMessageOptionsProperties = Record; +export interface IMessageOptions { + readonly customLanguages?: string[]; + readonly properties?: IMessageOptionsProperties; +} + +export type IMessageSetOptions = Omit; diff --git a/src/common/message/interfaces/message.service.interface.ts b/src/common/message/interfaces/message.service.interface.ts new file mode 100644 index 0000000..4c2047a --- /dev/null +++ b/src/common/message/interfaces/message.service.interface.ts @@ -0,0 +1,33 @@ +import { ValidationError } from '@nestjs/common'; +import { + IErrors, + IErrorsImport, + IValidationErrorImport, +} from 'src/common/error/interfaces/error.interface'; +import { + IMessage, + IMessageOptions, + IMessageSetOptions, +} from 'src/common/message/interfaces/message.interface'; + +export interface IMessageService { + getAvailableLanguages(): Promise; + + setMessage( + lang: string, + key: string, + options?: IMessageSetOptions + ): T; + + getRequestErrorsMessage( + requestErrors: ValidationError[], + customLanguages?: string[] + ): Promise; + + getImportErrorsMessage( + errors: IValidationErrorImport[], + customLanguages?: string[] + ): Promise; + + get(key: string, options?: IMessageOptions): Promise; +} diff --git a/src/common/message/message.module.ts b/src/common/message/message.module.ts new file mode 100644 index 0000000..5e200ad --- /dev/null +++ b/src/common/message/message.module.ts @@ -0,0 +1,36 @@ +import { Global, Module } from '@nestjs/common'; +import * as path from 'path'; +import { I18nModule, HeaderResolver, I18nJsonLoader } from 'nestjs-i18n'; +import { ConfigService } from '@nestjs/config'; +import { MessageService } from './services/message.service'; +import { ENUM_MESSAGE_LANGUAGE } from './constants/message.enum.constant'; +import { MessageMiddlewareModule } from 'src/common/message/middleware/message.middleware.module'; + +@Global() +@Module({ + providers: [MessageService], + exports: [MessageService], + imports: [ + I18nModule.forRootAsync({ + useFactory: (configService: ConfigService) => ({ + fallbackLanguage: configService + .get('app.language') + .join(','), + fallbacks: Object.values(ENUM_MESSAGE_LANGUAGE).reduce( + (a, v) => ({ ...a, [`${v}-*`]: v }), + {} + ), + loaderOptions: { + path: path.join(__dirname, '../../languages'), + watch: true, + }, + }), + loader: I18nJsonLoader, + inject: [ConfigService], + resolvers: [new HeaderResolver(['x-custom-lang'])], + }), + MessageMiddlewareModule, + ], + controllers: [], +}) +export class MessageModule {} diff --git a/src/common/message/middleware/custom-language/message.custom-language.middleware.ts b/src/common/message/middleware/custom-language/message.custom-language.middleware.ts new file mode 100644 index 0000000..da59bc5 --- /dev/null +++ b/src/common/message/middleware/custom-language/message.custom-language.middleware.ts @@ -0,0 +1,51 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Response, NextFunction } from 'express'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable() +export class MessageCustomLanguageMiddleware implements NestMiddleware { + private readonly appDefaultLanguage: string[]; + + constructor( + private readonly helperArrayService: HelperArrayService, + private readonly configService: ConfigService + ) { + this.appDefaultLanguage = + this.configService.get('app.language'); + } + + async use( + req: IRequestApp, + res: Response, + next: NextFunction + ): Promise { + let language: string = this.appDefaultLanguage.join(','); + let customLang: string[] = this.appDefaultLanguage; + + const reqLanguages: string = req.headers['x-custom-lang'] as string; + const enumLanguage: string[] = Object.values(ENUM_MESSAGE_LANGUAGE); + if (reqLanguages) { + const splitLanguage: string[] = reqLanguages + .split(',') + .map((val) => val.toLowerCase()); + const uniqueLanguage = + this.helperArrayService.unique(splitLanguage); + const languages: string[] = uniqueLanguage.filter((val) => + this.helperArrayService.includes(enumLanguage, val) + ); + + if (languages.length > 0) { + language = languages.join(','); + customLang = languages; + } + } + + req.__customLang = customLang; + req.headers['x-custom-lang'] = language; + + next(); + } +} diff --git a/src/common/message/middleware/message.middleware.module.ts b/src/common/message/middleware/message.middleware.module.ts new file mode 100644 index 0000000..7f1cd3e --- /dev/null +++ b/src/common/message/middleware/message.middleware.module.ts @@ -0,0 +1,9 @@ +import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; +import { MessageCustomLanguageMiddleware } from 'src/common/message/middleware/custom-language/message.custom-language.middleware'; + +@Module({}) +export class MessageMiddlewareModule implements NestModule { + configure(consumer: MiddlewareConsumer): void { + consumer.apply(MessageCustomLanguageMiddleware).forRoutes('*'); + } +} diff --git a/src/common/message/serializations/message.language.serialization.ts b/src/common/message/serializations/message.language.serialization.ts new file mode 100644 index 0000000..18edec9 --- /dev/null +++ b/src/common/message/serializations/message.language.serialization.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; + +export class MessageLanguageSerialization { + @ApiProperty({ + enum: ENUM_MESSAGE_LANGUAGE, + type: 'array', + isArray: true, + }) + language: ENUM_MESSAGE_LANGUAGE[]; +} diff --git a/src/common/message/services/message.service.ts b/src/common/message/services/message.service.ts new file mode 100644 index 0000000..2f3952a --- /dev/null +++ b/src/common/message/services/message.service.ts @@ -0,0 +1,140 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ValidationError } from 'class-validator'; +import { I18nService } from 'nestjs-i18n'; +import { + IErrors, + IErrorsImport, + IValidationErrorImport, +} from 'src/common/error/interfaces/error.interface'; +import { ENUM_MESSAGE_LANGUAGE } from 'src/common/message/constants/message.enum.constant'; +import { + IMessage, + IMessageOptions, + IMessageSetOptions, +} from 'src/common/message/interfaces/message.interface'; +import { IMessageService } from 'src/common/message/interfaces/message.service.interface'; + +@Injectable() +export class MessageService implements IMessageService { + private readonly appDefaultLanguage: string[]; + + constructor( + private readonly i18n: I18nService, + private readonly configService: ConfigService + ) { + this.appDefaultLanguage = + this.configService.get('app.language'); + } + + async getAvailableLanguages(): Promise { + return Object.values(ENUM_MESSAGE_LANGUAGE); + } + + setMessage( + lang: string, + key: string, + options?: IMessageSetOptions + ): T { + return this.i18n.translate(key, { + lang: lang ?? this.appDefaultLanguage.join(','), + args: options?.properties, + }) as T; + } + + async getRequestErrorsMessage( + requestErrors: ValidationError[], + customLanguages?: string[] + ): Promise { + const messages: Array = []; + for (const transfomer of requestErrors) { + let children: Record[] = transfomer.children; + let constraints: string[] = Object.keys( + transfomer.constraints ?? [] + ); + const errors: IErrors[] = []; + let property: string = transfomer.property; + let propertyValue: string = transfomer.value; + + if (children.length > 0) { + while (children.length > 0) { + for (const child of children) { + property = `${property}.${child.property}`; + + if (child.children?.length > 0) { + children = child.children; + break; + } else if (child.constraints) { + constraints = Object.keys(child.constraints); + children = []; + propertyValue = child.value; + break; + } + } + } + } + + for (const constraint of constraints) { + const message = await this.get(`request.${constraint}`, { + customLanguages, + properties: { + property, + value: propertyValue, + }, + }); + errors.push({ + property, + message, + }); + } + + messages.push(errors); + } + + return messages.flat(1) as IErrors[]; + } + + async getImportErrorsMessage( + errors: IValidationErrorImport[], + customLanguages?: string[] + ): Promise { + const newErrors: IErrorsImport[] = []; + for (const error of errors) { + newErrors.push({ + row: error.row, + file: error.file, + errors: await this.getRequestErrorsMessage( + error.errors, + customLanguages + ), + }); + } + + return newErrors; + } + + async get(key: string, options?: IMessageOptions): Promise { + const properties = options?.properties; + const customLanguages = + options?.customLanguages?.length > 0 + ? options.customLanguages + : this.appDefaultLanguage; + + const messages: IMessage = {}; + for (const customLanguage of customLanguages) { + messages[customLanguage] = await this.setMessage( + customLanguage, + key, + { + properties, + } + ); + } + + if (customLanguages.length <= 1) { + return messages[customLanguages[0]] as T; + } + + return messages as T; + } +} diff --git a/src/common/pagination/constants/pagination.constant.ts b/src/common/pagination/constants/pagination.constant.ts new file mode 100644 index 0000000..ebbfdcf --- /dev/null +++ b/src/common/pagination/constants/pagination.constant.ts @@ -0,0 +1,12 @@ +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; + +export const PAGINATION_PER_PAGE = 20; +export const PAGINATION_MAX_PER_PAGE = 100; +export const PAGINATION_PAGE = 1; +export const PAGINATION_MAX_PAGE = 20; +export const PAGINATION_ORDER_BY = 'createdAt'; +export const PAGINATION_ORDER_DIRECTION: ENUM_PAGINATION_ORDER_DIRECTION_TYPE = + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC; +export const PAGINATION_AVAILABLE_ORDER_BY: string[] = ['createdAt']; +export const PAGINATION_AVAILABLE_ORDER_DIRECTION: ENUM_PAGINATION_ORDER_DIRECTION_TYPE[] = + Object.values(ENUM_PAGINATION_ORDER_DIRECTION_TYPE); diff --git a/src/common/pagination/constants/pagination.enum.constant.ts b/src/common/pagination/constants/pagination.enum.constant.ts new file mode 100644 index 0000000..9e3b3cb --- /dev/null +++ b/src/common/pagination/constants/pagination.enum.constant.ts @@ -0,0 +1,14 @@ +export enum ENUM_PAGINATION_ORDER_DIRECTION_TYPE { + ASC = 'asc', + DESC = 'desc', +} + +export enum ENUM_PAGINATION_FILTER_CASE_OPTIONS { + UPPERCASE = 'UPPERCASE', + LOWERCASE = 'LOWERCASE', +} + +export enum ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS { + START_OF_DAY = 'START_OF_DAY', + END_OF_DAY = 'END_OF_DAY', +} diff --git a/src/common/pagination/decorators/pagination.decorator.ts b/src/common/pagination/decorators/pagination.decorator.ts new file mode 100644 index 0000000..29449b5 --- /dev/null +++ b/src/common/pagination/decorators/pagination.decorator.ts @@ -0,0 +1,85 @@ +import { Query } from '@nestjs/common'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { + IPaginationFilterDateOptions, + IPaginationFilterStringContainOptions, + IPaginationFilterStringEqualOptions, +} from 'src/common/pagination/interfaces/pagination.interface'; +import { PaginationFilterContainPipe } from 'src/common/pagination/pipes/pagination.filter-contain.pipe'; +import { PaginationFilterDatePipe } from 'src/common/pagination/pipes/pagination.filter-date.pipe'; +import { PaginationFilterEqualObjectIdPipe } from 'src/common/pagination/pipes/pagination.filter-equal-object-id.pipe'; +import { PaginationFilterEqualPipe } from 'src/common/pagination/pipes/pagination.filter-equal.pipe'; +import { PaginationFilterInBooleanPipe } from 'src/common/pagination/pipes/pagination.filter-in-boolean.pipe'; +import { PaginationFilterInEnumPipe } from 'src/common/pagination/pipes/pagination.filter-in-enum.pipe'; +import { PaginationOrderPipe } from 'src/common/pagination/pipes/pagination.order.pipe'; +import { PaginationPagingPipe } from 'src/common/pagination/pipes/pagination.paging.pipe'; +import { PaginationSearchPipe } from 'src/common/pagination/pipes/pagination.search.pipe'; + +export function PaginationQuery( + defaultPerPage: number, + defaultOrderBy: string, + defaultOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE, + availableSearch: string[], + availableOrderBy: string[] +): ParameterDecorator { + return Query( + PaginationSearchPipe(availableSearch), + PaginationPagingPipe(defaultPerPage), + PaginationOrderPipe( + defaultOrderBy, + defaultOrderDirection, + availableOrderBy + ) + ); +} + +export function PaginationQuerySearch( + availableSearch: string[] +): ParameterDecorator { + return Query(PaginationSearchPipe(availableSearch)); +} + +export function PaginationQueryFilterInBoolean( + field: string, + defaultValue: boolean[] +): ParameterDecorator { + return Query(field, PaginationFilterInBooleanPipe(defaultValue)); +} + +export function PaginationQueryFilterInEnum( + field: string, + defaultValue: T, + defaultEnum: Record +): ParameterDecorator { + return Query( + field, + PaginationFilterInEnumPipe(defaultValue, defaultEnum) + ); +} + +export function PaginationQueryFilterEqual( + field: string, + options?: IPaginationFilterStringEqualOptions +): ParameterDecorator { + return Query(field, PaginationFilterEqualPipe(options)); +} + +export function PaginationQueryFilterContain( + field: string, + options?: IPaginationFilterStringContainOptions +): ParameterDecorator { + return Query(field, PaginationFilterContainPipe(options)); +} + +export function PaginationQueryFilterDate( + field: string, + options?: IPaginationFilterDateOptions +): ParameterDecorator { + return Query(field, PaginationFilterDatePipe(options)); +} + +export function PaginationQueryFilterEqualObjectId( + field: string +): ParameterDecorator { + return Query(field, PaginationFilterEqualObjectIdPipe); +} diff --git a/src/common/pagination/dtos/pagination.list.dto.ts b/src/common/pagination/dtos/pagination.list.dto.ts new file mode 100644 index 0000000..7225b31 --- /dev/null +++ b/src/common/pagination/dtos/pagination.list.dto.ts @@ -0,0 +1,23 @@ +import { ApiHideProperty } from '@nestjs/swagger'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { IPaginationOrder } from 'src/common/pagination/interfaces/pagination.interface'; + +export class PaginationListDto { + @ApiHideProperty() + _search: Record; + + @ApiHideProperty() + _limit: number; + + @ApiHideProperty() + _offset: number; + + @ApiHideProperty() + _order: IPaginationOrder; + + @ApiHideProperty() + _availableOrderBy: string[]; + + @ApiHideProperty() + _availableOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE[]; +} diff --git a/src/common/pagination/interfaces/pagination.interface.ts b/src/common/pagination/interfaces/pagination.interface.ts new file mode 100644 index 0000000..a7cc8c0 --- /dev/null +++ b/src/common/pagination/interfaces/pagination.interface.ts @@ -0,0 +1,35 @@ +import { + ENUM_PAGINATION_FILTER_CASE_OPTIONS, + ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS, + ENUM_PAGINATION_ORDER_DIRECTION_TYPE, +} from 'src/common/pagination/constants/pagination.enum.constant'; + +export type IPaginationOrder = Record< + string, + ENUM_PAGINATION_ORDER_DIRECTION_TYPE +>; + +export interface IPaginationPaging { + limit: number; + offset: number; +} + +export interface IPaginationOptions { + paging?: IPaginationPaging; + order?: IPaginationOrder; +} + +export interface IPaginationFilterDateOptions { + time?: ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS; +} + +export interface IPaginationFilterStringContainOptions { + case?: ENUM_PAGINATION_FILTER_CASE_OPTIONS; + trim?: boolean; + fullMatch?: boolean; +} + +export interface IPaginationFilterStringEqualOptions + extends IPaginationFilterStringContainOptions { + isNumber?: boolean; +} diff --git a/src/common/pagination/interfaces/pagination.service.interface.ts b/src/common/pagination/interfaces/pagination.service.interface.ts new file mode 100644 index 0000000..f0ee902 --- /dev/null +++ b/src/common/pagination/interfaces/pagination.service.interface.ts @@ -0,0 +1,43 @@ +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { IPaginationOrder } from 'src/common/pagination/interfaces/pagination.interface'; + +export interface IPaginationService { + offset(page: number, perPage: number): number; + + totalPage(totalData: number, perPage: number): number; + + offsetWithoutMax(page: number, perPage: number): number; + + totalPageWithoutMax(totalData: number, perPage: number): number; + + page(page: number): number; + + perPage(perPage: number): number; + + order( + orderByValue: string, + orderDirectionValue: ENUM_PAGINATION_ORDER_DIRECTION_TYPE, + availableOrderBy: string[], + availableOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE[] + ): IPaginationOrder; + + search( + searchValue: string, + availableSearch: string[] + ): Record | undefined; + + filterEqual( + field: string, + filterValue?: T + ): Record | undefined; + + filterContain( + field: string, + filterValue?: string + ): Record | undefined; + + filterIn( + field: string, + filterValue?: T[] + ): Record | undefined; +} diff --git a/src/common/pagination/pagination.module.ts b/src/common/pagination/pagination.module.ts new file mode 100644 index 0000000..ec702e2 --- /dev/null +++ b/src/common/pagination/pagination.module.ts @@ -0,0 +1,10 @@ +import { Global, Module } from '@nestjs/common'; +import { PaginationService } from './services/pagination.service'; + +@Global() +@Module({ + providers: [PaginationService], + exports: [PaginationService], + imports: [], +}) +export class PaginationModule {} diff --git a/src/common/pagination/pipes/pagination.filter-contain.pipe.ts b/src/common/pagination/pipes/pagination.filter-contain.pipe.ts new file mode 100644 index 0000000..3ce8ffc --- /dev/null +++ b/src/common/pagination/pipes/pagination.filter-contain.pipe.ts @@ -0,0 +1,62 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { + ArgumentMetadata, + PipeTransform, + Scope, +} from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { ENUM_PAGINATION_FILTER_CASE_OPTIONS } from 'src/common/pagination/constants/pagination.enum.constant'; +import { IPaginationFilterStringContainOptions } from 'src/common/pagination/interfaces/pagination.interface'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function PaginationFilterContainPipe( + options?: IPaginationFilterStringContainOptions +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationFilterContainPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService + ) {} + + async transform( + value: string, + { data: field }: ArgumentMetadata + ): Promise> { + if (!value) { + value = ''; + } + + if ( + options?.case === ENUM_PAGINATION_FILTER_CASE_OPTIONS.UPPERCASE + ) { + value = value.toUpperCase(); + } else if ( + options?.case === ENUM_PAGINATION_FILTER_CASE_OPTIONS.LOWERCASE + ) { + value = value.toUpperCase(); + } + + if (options?.trim) { + value = value.trim(); + } + + if (options?.fullMatch) { + return this.paginationService.filterContainFullMatch( + field, + value + ); + } + + this.request.__filters = { + ...this.request.__filters, + [field]: value, + }; + + return this.paginationService.filterContain(field, value); + } + } + + return mixin(MixinPaginationFilterContainPipe); +} diff --git a/src/common/pagination/pipes/pagination.filter-date.pipe.ts b/src/common/pagination/pipes/pagination.filter-date.pipe.ts new file mode 100644 index 0000000..71dadf3 --- /dev/null +++ b/src/common/pagination/pipes/pagination.filter-date.pipe.ts @@ -0,0 +1,53 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { + ArgumentMetadata, + PipeTransform, + Scope, +} from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS } from 'src/common/pagination/constants/pagination.enum.constant'; +import { IPaginationFilterDateOptions } from 'src/common/pagination/interfaces/pagination.interface'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function PaginationFilterDatePipe( + options?: IPaginationFilterDateOptions +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationFilterDatePipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService, + private readonly helperDateService: HelperDateService + ) {} + + async transform( + value: string, + { data: field }: ArgumentMetadata + ): Promise> { + let date: Date = this.helperDateService.create(value); + + if ( + options?.time === + ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS.END_OF_DAY + ) { + date = this.helperDateService.endOfDay(date); + } else if ( + options?.time === + ENUM_PAGINATION_FILTER_DATE_TIME_OPTIONS.START_OF_DAY + ) { + date = this.helperDateService.startOfDay(date); + } + + this.request.__filters = { + ...this.request.__filters, + [field]: value, + }; + + return this.paginationService.filterDate(field, date); + } + } + + return mixin(MixinPaginationFilterDatePipe); +} diff --git a/src/common/pagination/pipes/pagination.filter-equal-object-id.pipe.ts b/src/common/pagination/pipes/pagination.filter-equal-object-id.pipe.ts new file mode 100644 index 0000000..55073fa --- /dev/null +++ b/src/common/pagination/pipes/pagination.filter-equal-object-id.pipe.ts @@ -0,0 +1,42 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + ArgumentMetadata, + PipeTransform, + Scope, +} from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { Types } from 'mongoose'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable({ scope: Scope.REQUEST }) +export class PaginationFilterEqualObjectIdPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService + ) {} + + async transform( + value: string, + { data: field }: ArgumentMetadata + ): Promise> { + if (!value) { + return undefined; + } + + value = value.trim(); + const finalValue = Types.ObjectId.isValid(value) + ? new Types.ObjectId(value) + : value; + + this.request.__filters = { + ...this.request.__filters, + [field]: value, + }; + + return this.paginationService.filterEqual( + field, + finalValue + ); + } +} diff --git a/src/common/pagination/pipes/pagination.filter-equal.pipe.ts b/src/common/pagination/pipes/pagination.filter-equal.pipe.ts new file mode 100644 index 0000000..84d8b22 --- /dev/null +++ b/src/common/pagination/pipes/pagination.filter-equal.pipe.ts @@ -0,0 +1,67 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { + ArgumentMetadata, + PipeTransform, + Scope, +} from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; +import { ENUM_PAGINATION_FILTER_CASE_OPTIONS } from 'src/common/pagination/constants/pagination.enum.constant'; +import { IPaginationFilterStringEqualOptions } from 'src/common/pagination/interfaces/pagination.interface'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function PaginationFilterEqualPipe( + options?: IPaginationFilterStringEqualOptions +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationFilterEqualPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService, + private readonly helperNumberService: HelperNumberService + ) {} + + async transform( + value: string, + { data: field }: ArgumentMetadata + ): Promise> { + if (!value) { + return undefined; + } + + if ( + options?.case === ENUM_PAGINATION_FILTER_CASE_OPTIONS.UPPERCASE + ) { + value = value.toUpperCase(); + } else if ( + options?.case === ENUM_PAGINATION_FILTER_CASE_OPTIONS.LOWERCASE + ) { + value = value.toUpperCase(); + } + + if (options?.trim) { + value = value.trim(); + } + + let finalValue: string | number = value; + if (options?.isNumber) { + finalValue = this.helperNumberService.check(value) + ? this.helperNumberService.create(value) + : value; + } + + this.request.__filters = { + ...this.request.__filters, + [field]: finalValue, + }; + + return this.paginationService.filterEqual( + field, + finalValue + ); + } + } + + return mixin(MixinPaginationFilterEqualPipe); +} diff --git a/src/common/pagination/pipes/pagination.filter-in-boolean.pipe.ts b/src/common/pagination/pipes/pagination.filter-in-boolean.pipe.ts new file mode 100644 index 0000000..3665e73 --- /dev/null +++ b/src/common/pagination/pipes/pagination.filter-in-boolean.pipe.ts @@ -0,0 +1,45 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { + ArgumentMetadata, + PipeTransform, + Scope, +} from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function PaginationFilterInBooleanPipe( + defaultValue: boolean[] +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationFilterInBooleanPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService, + private readonly helperArrayService: HelperArrayService + ) {} + + async transform( + value: string, + { data: field }: ArgumentMetadata + ): Promise> { + let finalValue: boolean[] = defaultValue as boolean[]; + + if (value) { + finalValue = this.helperArrayService.unique( + value.split(',').map((val: string) => val === 'true') + ); + } + + this.request.__filters = { + ...this.request.__filters, + [field]: finalValue, + }; + + return this.paginationService.filterIn(field, finalValue); + } + } + + return mixin(MixinPaginationFilterInBooleanPipe); +} diff --git a/src/common/pagination/pipes/pagination.filter-in-enum.pipe.ts b/src/common/pagination/pipes/pagination.filter-in-enum.pipe.ts new file mode 100644 index 0000000..e7b8a1c --- /dev/null +++ b/src/common/pagination/pipes/pagination.filter-in-enum.pipe.ts @@ -0,0 +1,45 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { + ArgumentMetadata, + PipeTransform, + Scope, +} from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function PaginationFilterInEnumPipe( + defaultValue: T, + defaultEnum: Record +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationFilterInEnumPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService + ) {} + + async transform( + value: string, + { data: field }: ArgumentMetadata + ): Promise> { + let finalValue: T[] = defaultValue as T[]; + + if (value) { + finalValue = value + .split(',') + .map((val: string) => defaultEnum[val]) + .filter((val: string) => val) as T[]; + } + + this.request.__filters = { + ...this.request.__filters, + [field]: finalValue as string[], + }; + + return this.paginationService.filterIn(field, finalValue); + } + } + + return mixin(MixinPaginationFilterInEnumPipe); +} diff --git a/src/common/pagination/pipes/pagination.order.pipe.ts b/src/common/pagination/pipes/pagination.order.pipe.ts new file mode 100644 index 0000000..81c1fc5 --- /dev/null +++ b/src/common/pagination/pipes/pagination.order.pipe.ts @@ -0,0 +1,54 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { PipeTransform, Scope } from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { PAGINATION_AVAILABLE_ORDER_DIRECTION } from 'src/common/pagination/constants/pagination.constant'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function PaginationOrderPipe( + defaultOrderBy: string, + defaultOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE, + availableOrderBy: string[] +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationOrderPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService + ) {} + + async transform( + value: Record + ): Promise> { + const orderBy: string = value?.orderBy ?? defaultOrderBy; + const orderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE = + value?.orderDirection ?? defaultOrderDirection; + const availableOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE[] = + PAGINATION_AVAILABLE_ORDER_DIRECTION; + + const order: Record = this.paginationService.order( + orderBy, + orderDirection, + availableOrderBy + ); + + this.request.__pagination = { + ...this.request.__pagination, + orderBy, + orderDirection, + availableOrderBy, + availableOrderDirection, + }; + + return { + ...value, + _order: order, + _availableOrderBy: availableOrderBy, + _availableOrderDirection: availableOrderDirection, + }; + } + } + + return mixin(MixinPaginationOrderPipe); +} diff --git a/src/common/pagination/pipes/pagination.paging.pipe.ts b/src/common/pagination/pipes/pagination.paging.pipe.ts new file mode 100644 index 0000000..eece330 --- /dev/null +++ b/src/common/pagination/pipes/pagination.paging.pipe.ts @@ -0,0 +1,49 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { PipeTransform, Scope } from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function PaginationPagingPipe( + defaultPerPage: number +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationPagingPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService, + private readonly helperNumberService: HelperNumberService + ) {} + + async transform( + value: Record + ): Promise> { + const page: number = this.paginationService.page( + this.helperNumberService.create(value?.page ?? 1) + ); + const perPage: number = this.paginationService.perPage( + this.helperNumberService.create( + value?.perPage ?? defaultPerPage + ) + ); + const offset: number = this.paginationService.offset(page, perPage); + + this.request.__pagination = { + ...this.request.__pagination, + page, + perPage, + }; + + return { + ...value, + page, + perPage, + _limit: perPage, + _offset: offset, + }; + } + } + + return mixin(MixinPaginationPagingPipe); +} diff --git a/src/common/pagination/pipes/pagination.search.pipe.ts b/src/common/pagination/pipes/pagination.search.pipe.ts new file mode 100644 index 0000000..67c7314 --- /dev/null +++ b/src/common/pagination/pipes/pagination.search.pipe.ts @@ -0,0 +1,41 @@ +import { Inject, Injectable, mixin, Type } from '@nestjs/common'; +import { PipeTransform, Scope } from '@nestjs/common/interfaces'; +import { REQUEST } from '@nestjs/core'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +export function PaginationSearchPipe( + availableSearch: string[] +): Type { + @Injectable({ scope: Scope.REQUEST }) + class MixinPaginationSearchPipe implements PipeTransform { + constructor( + @Inject(REQUEST) protected readonly request: IRequestApp, + private readonly paginationService: PaginationService + ) {} + + async transform( + value: Record + ): Promise> { + const searchText = value?.search ?? ''; + const search: Record = this.paginationService.search( + value?.search, + availableSearch + ); + + this.request.__pagination = { + ...this.request.__pagination, + search: searchText, + availableSearch, + }; + + return { + ...value, + _search: search, + _availableSearch: availableSearch, + }; + } + } + + return mixin(MixinPaginationSearchPipe); +} diff --git a/src/common/pagination/services/pagination.service.ts b/src/common/pagination/services/pagination.service.ts new file mode 100644 index 0000000..c6f7a1a --- /dev/null +++ b/src/common/pagination/services/pagination.service.ts @@ -0,0 +1,136 @@ +import { Injectable } from '@nestjs/common'; +import { + PAGINATION_AVAILABLE_ORDER_BY, + PAGINATION_MAX_PAGE, + PAGINATION_MAX_PER_PAGE, + PAGINATION_ORDER_BY, + PAGINATION_ORDER_DIRECTION, + PAGINATION_PAGE, + PAGINATION_PER_PAGE, +} from 'src/common/pagination/constants/pagination.constant'; +import { IPaginationOrder } from 'src/common/pagination/interfaces/pagination.interface'; +import { IPaginationService } from 'src/common/pagination/interfaces/pagination.service.interface'; + +@Injectable() +export class PaginationService implements IPaginationService { + offset(page: number, perPage: number): number { + page = page > PAGINATION_MAX_PAGE ? PAGINATION_MAX_PAGE : page; + perPage = + perPage > PAGINATION_MAX_PER_PAGE + ? PAGINATION_MAX_PER_PAGE + : perPage; + const offset: number = (page - 1) * perPage; + + return offset; + } + + totalPage(totalData: number, perPage: number): number { + let totalPage = Math.ceil(totalData / perPage); + totalPage = totalPage === 0 ? 1 : totalPage; + return totalPage > PAGINATION_MAX_PAGE + ? PAGINATION_MAX_PAGE + : totalPage; + } + + offsetWithoutMax(page: number, perPage: number): number { + const offset: number = (page - 1) * perPage; + return offset; + } + + totalPageWithoutMax(totalData: number, perPage: number): number { + let totalPage = Math.ceil(totalData / perPage); + totalPage = totalPage === 0 ? 1 : totalPage; + return totalPage; + } + + page(page?: number): number { + return page + ? page > PAGINATION_MAX_PAGE + ? PAGINATION_MAX_PAGE + : page + : PAGINATION_PAGE; + } + + perPage(perPage?: number): number { + return perPage + ? perPage > PAGINATION_MAX_PER_PAGE + ? PAGINATION_MAX_PER_PAGE + : perPage + : PAGINATION_PER_PAGE; + } + + order( + orderByValue = PAGINATION_ORDER_BY, + orderDirectionValue = PAGINATION_ORDER_DIRECTION, + availableOrderBy = PAGINATION_AVAILABLE_ORDER_BY + ): IPaginationOrder { + const orderBy: string = availableOrderBy.includes(orderByValue) + ? orderByValue + : PAGINATION_ORDER_BY; + + return { [orderBy]: orderDirectionValue }; + } + + search( + searchValue = '', + availableSearch: string[] + ): Record | undefined { + if (!searchValue) { + return undefined; + } + + return { + $or: availableSearch.map((val) => ({ + [val]: { + $regex: new RegExp(searchValue), + $options: 'i', + }, + })), + }; + } + + filterEqual(field: string, filterValue: T): Record { + return { [field]: filterValue }; + } + + filterContain( + field: string, + filterValue: string + ): Record { + return { + [field]: { + $regex: new RegExp(filterValue), + $options: 'i', + }, + }; + } + + filterContainFullMatch( + field: string, + filterValue: string + ): Record { + return { + [field]: { + $regex: new RegExp(`\\b${filterValue}\\b`), + $options: 'i', + }, + }; + } + + filterIn( + field: string, + filterValue: T[] + ): Record { + return { + [field]: { + $in: filterValue, + }, + }; + } + + filterDate(field: string, filterValue: Date): Record { + return { + [field]: filterValue, + }; + } +} diff --git a/src/common/request/constants/request.constant.ts b/src/common/request/constants/request.constant.ts new file mode 100644 index 0000000..bf14ba4 --- /dev/null +++ b/src/common/request/constants/request.constant.ts @@ -0,0 +1,5 @@ +export const REQUEST_PARAM_CLASS_DTOS_META_KEY = 'RequestParamClassDtosMetaKey'; + +export const REQUEST_CUSTOM_TIMEOUT_META_KEY = 'RequestCustomTimeoutMetaKey'; +export const REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY = + 'RequestCustomTimeoutValueMetaKey'; diff --git a/src/common/request/constants/request.enum.constant.ts b/src/common/request/constants/request.enum.constant.ts new file mode 100644 index 0000000..52a5457 --- /dev/null +++ b/src/common/request/constants/request.enum.constant.ts @@ -0,0 +1,9 @@ +export enum ENUM_REQUEST_METHOD { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + PATCH = 'PATCH', + DELETE = 'DELETE', + OPTIONS = 'OPTIONS', + HEAD = 'HEAD', +} diff --git a/src/common/request/constants/request.status-code.constant.ts b/src/common/request/constants/request.status-code.constant.ts new file mode 100644 index 0000000..f19d64e --- /dev/null +++ b/src/common/request/constants/request.status-code.constant.ts @@ -0,0 +1,7 @@ +export enum ENUM_REQUEST_STATUS_CODE_ERROR { + REQUEST_VALIDATION_ERROR = 5981, + REQUEST_TIMESTAMP_INVALID_ERROR = 5982, + REQUEST_USER_AGENT_INVALID_ERROR = 5983, + REQUEST_USER_AGENT_OS_INVALID_ERROR = 5984, + REQUEST_USER_AGENT_BROWSER_INVALID_ERROR = 5985, +} diff --git a/src/common/request/decorators/request.decorator.ts b/src/common/request/decorators/request.decorator.ts new file mode 100644 index 0000000..4be89bb --- /dev/null +++ b/src/common/request/decorators/request.decorator.ts @@ -0,0 +1,78 @@ +import { + applyDecorators, + createParamDecorator, + ExecutionContext, + SetMetadata, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { ClassConstructor } from 'class-transformer'; +import { + REQUEST_CUSTOM_TIMEOUT_META_KEY, + REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY, + REQUEST_PARAM_CLASS_DTOS_META_KEY, +} from 'src/common/request/constants/request.constant'; +import { RequestParamRawGuard } from 'src/common/request/guards/request.param.guard'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { IResult } from 'ua-parser-js'; +import { RequestTimestampInterceptor } from 'src/common/request/interceptors/request.timestamp.interceptor'; +import { RequestUserAgentInterceptor } from 'src/common/request/interceptors/request.user-agent.interceptor'; + +export const RequestUserAgent: () => ParameterDecorator = createParamDecorator( + (data: string, ctx: ExecutionContext): IResult => { + const { __userAgent } = ctx.switchToHttp().getRequest() as IRequestApp; + return __userAgent; + } +); + +export const RequestId: () => ParameterDecorator = createParamDecorator( + (data: string, ctx: ExecutionContext): string => { + const { __id } = ctx.switchToHttp().getRequest() as IRequestApp; + return __id; + } +); + +export const RequestXTimestamp: () => ParameterDecorator = createParamDecorator( + (data: string, ctx: ExecutionContext): number => { + const { __xTimestamp } = ctx.switchToHttp().getRequest() as IRequestApp; + return __xTimestamp; + } +); + +export const RequestTimestamp: () => ParameterDecorator = createParamDecorator( + (data: string, ctx: ExecutionContext): number => { + const { __timestamp } = ctx.switchToHttp().getRequest() as IRequestApp; + return __timestamp; + } +); + +export const RequestCustomLang: () => ParameterDecorator = createParamDecorator( + (data: string, ctx: ExecutionContext): string[] => { + const { __customLang } = ctx.switchToHttp().getRequest() as IRequestApp; + return __customLang; + } +); + +export function RequestParamGuard( + ...classValidation: ClassConstructor[] +): MethodDecorator { + return applyDecorators( + UseGuards(RequestParamRawGuard), + SetMetadata(REQUEST_PARAM_CLASS_DTOS_META_KEY, classValidation) + ); +} + +export function RequestValidateUserAgent(): MethodDecorator { + return applyDecorators(UseInterceptors(RequestUserAgentInterceptor)); +} + +export function RequestValidateTimestamp(): MethodDecorator { + return applyDecorators(UseInterceptors(RequestTimestampInterceptor)); +} + +export function RequestTimeout(seconds: string): MethodDecorator { + return applyDecorators( + SetMetadata(REQUEST_CUSTOM_TIMEOUT_META_KEY, true), + SetMetadata(REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY, seconds) + ); +} diff --git a/src/common/request/guards/request.param.guard.ts b/src/common/request/guards/request.param.guard.ts new file mode 100644 index 0000000..df7afa7 --- /dev/null +++ b/src/common/request/guards/request.param.guard.ts @@ -0,0 +1,40 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ClassConstructor, plainToInstance } from 'class-transformer'; +import { validate, ValidationError } from 'class-validator'; +import { REQUEST_PARAM_CLASS_DTOS_META_KEY } from 'src/common/request/constants/request.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; + +@Injectable() +export class RequestParamRawGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const { params } = context.switchToHttp().getRequest(); + const classDtos: ClassConstructor[] = this.reflector.get< + ClassConstructor[] + >(REQUEST_PARAM_CLASS_DTOS_META_KEY, context.getHandler()); + + for (const clsDto of classDtos) { + const request = plainToInstance(clsDto, params); + + const errors: ValidationError[] = await validate(request); + + if (errors.length > 0) { + throw new BadRequestException({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR, + message: 'http.clientError.badRequest', + errors: errors, + }); + } + } + + return true; + } +} diff --git a/src/common/request/interceptors/request.timeout.interceptor.ts b/src/common/request/interceptors/request.timeout.interceptor.ts new file mode 100644 index 0000000..84ceaf4 --- /dev/null +++ b/src/common/request/interceptors/request.timeout.interceptor.ts @@ -0,0 +1,81 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + RequestTimeoutException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Reflector } from '@nestjs/core'; +import { Observable, throwError, TimeoutError } from 'rxjs'; +import { catchError, timeout } from 'rxjs/operators'; +import ms from 'ms'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { + REQUEST_CUSTOM_TIMEOUT_META_KEY, + REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY, +} from 'src/common/request/constants/request.constant'; + +@Injectable() +export class RequestTimeoutInterceptor + implements NestInterceptor> +{ + private readonly maxTimeoutInSecond: number; + + constructor( + private readonly configService: ConfigService, + private readonly reflector: Reflector + ) { + this.maxTimeoutInSecond = + this.configService.get('request.timeout'); + } + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise | string>> { + if (context.getType() === 'http') { + const customTimeout = this.reflector.get( + REQUEST_CUSTOM_TIMEOUT_META_KEY, + context.getHandler() + ); + + if (customTimeout) { + const seconds: string = this.reflector.get( + REQUEST_CUSTOM_TIMEOUT_VALUE_META_KEY, + context.getHandler() + ); + + return next.handle().pipe( + timeout(ms(seconds)), + catchError((err) => { + if (err instanceof TimeoutError) { + throw new RequestTimeoutException({ + statusCode: + ENUM_ERROR_STATUS_CODE_ERROR.ERROR_REQUEST_TIMEOUT, + message: 'http.clientError.requestTimeOut', + }); + } + return throwError(() => err); + }) + ); + } else { + return next.handle().pipe( + timeout(this.maxTimeoutInSecond), + catchError((err) => { + if (err instanceof TimeoutError) { + throw new RequestTimeoutException({ + statusCode: + ENUM_ERROR_STATUS_CODE_ERROR.ERROR_REQUEST_TIMEOUT, + message: 'http.clientError.requestTimeOut', + }); + } + return throwError(() => err); + }) + ); + } + } + + return next.handle(); + } +} diff --git a/src/common/request/interceptors/request.timestamp.interceptor.ts b/src/common/request/interceptors/request.timestamp.interceptor.ts new file mode 100644 index 0000000..2875731 --- /dev/null +++ b/src/common/request/interceptors/request.timestamp.interceptor.ts @@ -0,0 +1,76 @@ +import { + CallHandler, + ExecutionContext, + ForbiddenException, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Observable } from 'rxjs'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable() +export class RequestTimestampInterceptor + implements NestInterceptor> +{ + private readonly maxRequestTimestampInMs: number; + + constructor( + private readonly configService: ConfigService, + private readonly helperDateService: HelperDateService + ) { + this.maxRequestTimestampInMs = this.configService.get( + 'request.timestamp.toleranceTimeInMs' + ); + } + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise | string>> { + if (context.getType() === 'http') { + const request: IRequestApp = context.switchToHttp().getRequest(); + const timestamp: number = request.__timestamp; + + if (!timestamp) { + throw new ForbiddenException({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_TIMESTAMP_INVALID_ERROR, + message: 'auth.apiKey.error.timestampInvalid', + }); + } + + const checkTimestamp = + this.helperDateService.checkTimestamp(timestamp); + + if (!timestamp || !checkTimestamp) { + throw new ForbiddenException({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_TIMESTAMP_INVALID_ERROR, + message: 'request.error.timestampInvalid', + }); + } + + const timestampDate = this.helperDateService.create(timestamp); + + const toleranceMin = this.helperDateService.backwardInMilliseconds( + this.maxRequestTimestampInMs + ); + const toleranceMax = this.helperDateService.forwardInMilliseconds( + this.maxRequestTimestampInMs + ); + + if (timestampDate < toleranceMin || timestampDate > toleranceMax) { + throw new ForbiddenException({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_TIMESTAMP_INVALID_ERROR, + message: 'request.error.timestampInvalid', + }); + } + } + + return next.handle(); + } +} diff --git a/src/common/request/interceptors/request.user-agent.interceptor.ts b/src/common/request/interceptors/request.user-agent.interceptor.ts new file mode 100644 index 0000000..3ee848f --- /dev/null +++ b/src/common/request/interceptors/request.user-agent.interceptor.ts @@ -0,0 +1,68 @@ +import { + CallHandler, + ExecutionContext, + ForbiddenException, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Observable } from 'rxjs'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { IResult } from 'ua-parser-js'; + +@Injectable() +export class RequestUserAgentInterceptor + implements NestInterceptor> +{ + private readonly userAgentOs: string[]; + private readonly userAgentBrowser: string[]; + + constructor(private readonly configService: ConfigService) { + this.userAgentBrowser = this.configService.get( + 'request.userAgent.browser' + ); + this.userAgentOs = this.configService.get( + 'request.userAgent.os' + ); + } + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise | string>> { + if (context.getType() === 'http') { + const request: IRequestApp = context + .switchToHttp() + .getRequest(); + + const userAgent: IResult = request.__userAgent; + + if ( + !this.userAgentOs.some((val) => + val.match(new RegExp(userAgent.os.name)) + ) + ) { + throw new ForbiddenException({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_OS_INVALID_ERROR, + message: 'request.error.userAgentOsInvalid', + }); + } + + if ( + !this.userAgentBrowser.some((val) => + val.match(new RegExp(userAgent.browser.name)) + ) + ) { + throw new ForbiddenException({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_USER_AGENT_BROWSER_INVALID_ERROR, + message: 'request.error.userAgentBrowserInvalid', + }); + } + } + + return next.handle(); + } +} diff --git a/src/common/request/interfaces/request.interface.ts b/src/common/request/interfaces/request.interface.ts new file mode 100644 index 0000000..e859dee --- /dev/null +++ b/src/common/request/interfaces/request.interface.ts @@ -0,0 +1,27 @@ +import { Request } from 'express'; +import { IApiKeyPayload } from 'src/common/api-key/interfaces/api-key.interface'; +import { RequestPaginationSerialization } from 'src/common/request/serializations/request.pagination.serialization'; +import { IResult } from 'ua-parser-js'; + +export interface IRequestApp extends Request { + apiKey?: IApiKeyPayload; + user?: Record; + + __id: string; + __xTimestamp?: number; + __timestamp: number; + __timezone: string; + __customLang: string[]; + __version: string; + __repoVersion: string; + __userAgent: IResult; + + __class?: string; + __function?: string; + + __filters?: Record< + string, + string | number | boolean | Array + >; + __pagination?: RequestPaginationSerialization; +} diff --git a/src/common/request/middleware/body-parser/request.body-parser.middleware.ts b/src/common/request/middleware/body-parser/request.body-parser.middleware.ts new file mode 100644 index 0000000..fe9ffd2 --- /dev/null +++ b/src/common/request/middleware/body-parser/request.body-parser.middleware.ts @@ -0,0 +1,73 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import bodyParser from 'body-parser'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class RequestUrlencodedBodyParserMiddleware implements NestMiddleware { + private readonly maxFile: number; + + constructor(private readonly configService: ConfigService) { + this.maxFile = this.configService.get( + 'request.body.urlencoded.maxFileSize' + ); + } + + use(req: Request, res: Response, next: NextFunction): void { + bodyParser.urlencoded({ + extended: false, + limit: this.maxFile, + })(req, res, next); + } +} + +@Injectable() +export class RequestJsonBodyParserMiddleware implements NestMiddleware { + private readonly maxFile: number; + + constructor(private readonly configService: ConfigService) { + this.maxFile = this.configService.get( + 'request.body.json.maxFileSize' + ); + } + + use(req: Request, res: Response, next: NextFunction): void { + bodyParser.json({ + limit: this.maxFile, + })(req, res, next); + } +} + +@Injectable() +export class RequestRawBodyParserMiddleware implements NestMiddleware { + private readonly maxFile: number; + + constructor(private readonly configService: ConfigService) { + this.maxFile = this.configService.get( + 'request.body.raw.maxFileSize' + ); + } + + use(req: Request, res: Response, next: NextFunction): void { + bodyParser.raw({ + limit: this.maxFile, + })(req, res, next); + } +} + +@Injectable() +export class RequestTextBodyParserMiddleware implements NestMiddleware { + private readonly maxFile: number; + + constructor(private readonly configService: ConfigService) { + this.maxFile = this.configService.get( + 'request.body.text.maxFileSize' + ); + } + + use(req: Request, res: Response, next: NextFunction): void { + bodyParser.text({ + limit: this.maxFile, + })(req, res, next); + } +} diff --git a/src/common/request/middleware/cors/request.cors.middleware.ts b/src/common/request/middleware/cors/request.cors.middleware.ts new file mode 100644 index 0000000..4195bea --- /dev/null +++ b/src/common/request/middleware/cors/request.cors.middleware.ts @@ -0,0 +1,44 @@ +import { HttpStatus, Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import cors, { CorsOptions } from 'cors'; +import { ConfigService } from '@nestjs/config'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant'; + +@Injectable() +export class RequestCorsMiddleware implements NestMiddleware { + private readonly appEnv: ENUM_APP_ENVIRONMENT; + private readonly allowOrigin: string | boolean | string[]; + private readonly allowMethod: string[]; + private readonly allowHeader: string[]; + + constructor(private readonly configService: ConfigService) { + this.appEnv = this.configService.get('app.env'); + this.allowOrigin = this.configService.get( + 'request.cors.allowOrigin' + ); + this.allowMethod = this.configService.get( + 'request.cors.allowMethod' + ); + this.allowHeader = this.configService.get( + 'request.cors.allowHeader' + ); + } + + use(req: Request, res: Response, next: NextFunction): void { + const allowOrigin = + this.appEnv === ENUM_APP_ENVIRONMENT.PRODUCTION + ? this.allowOrigin + : '*'; + + const corsOptions: CorsOptions = { + origin: allowOrigin, + methods: this.allowMethod, + allowedHeaders: this.allowHeader, + preflightContinue: false, + credentials: true, + optionsSuccessStatus: HttpStatus.NO_CONTENT, + }; + + cors(corsOptions)(req, res, next); + } +} diff --git a/src/common/request/middleware/helmet/request.helmet.middleware.ts b/src/common/request/middleware/helmet/request.helmet.middleware.ts new file mode 100644 index 0000000..b526e01 --- /dev/null +++ b/src/common/request/middleware/helmet/request.helmet.middleware.ts @@ -0,0 +1,10 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import helmet from 'helmet'; + +@Injectable() +export class RequestHelmetMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction): void { + helmet()(req, res, next); + } +} diff --git a/src/common/request/middleware/id/request.id.middleware.ts b/src/common/request/middleware/id/request.id.middleware.ts new file mode 100644 index 0000000..df6a6e1 --- /dev/null +++ b/src/common/request/middleware/id/request.id.middleware.ts @@ -0,0 +1,18 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Response, NextFunction } from 'express'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable() +export class RequestIdMiddleware implements NestMiddleware { + async use( + req: IRequestApp, + res: Response, + next: NextFunction + ): Promise { + const uuid: string = DatabaseDefaultUUID(); + + req.__id = uuid; + next(); + } +} diff --git a/src/common/request/middleware/request.middleware.module.ts b/src/common/request/middleware/request.middleware.module.ts new file mode 100644 index 0000000..a82f18a --- /dev/null +++ b/src/common/request/middleware/request.middleware.module.ts @@ -0,0 +1,36 @@ +import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; +import { + RequestJsonBodyParserMiddleware, + RequestRawBodyParserMiddleware, + RequestTextBodyParserMiddleware, + RequestUrlencodedBodyParserMiddleware, +} from 'src/common/request/middleware/body-parser/request.body-parser.middleware'; +import { RequestCorsMiddleware } from 'src/common/request/middleware/cors/request.cors.middleware'; +import { RequestHelmetMiddleware } from 'src/common/request/middleware/helmet/request.helmet.middleware'; +import { RequestIdMiddleware } from 'src/common/request/middleware/id/request.id.middleware'; +import { RequestTimestampMiddleware } from 'src/common/request/middleware/timestamp/request.timestamp.middleware'; +import { RequestTimezoneMiddleware } from 'src/common/request/middleware/timezone/request.timezone.middleware'; +import { RequestUserAgentMiddleware } from 'src/common/request/middleware/user-agent/request.user-agent.middleware'; + +import { RequestVersionMiddleware } from 'src/common/request/middleware/version/request.version.middleware'; + +@Module({}) +export class RequestMiddlewareModule implements NestModule { + configure(consumer: MiddlewareConsumer): void { + consumer + .apply( + RequestHelmetMiddleware, + RequestIdMiddleware, + RequestJsonBodyParserMiddleware, + RequestTextBodyParserMiddleware, + RequestRawBodyParserMiddleware, + RequestUrlencodedBodyParserMiddleware, + RequestCorsMiddleware, + RequestVersionMiddleware, + RequestUserAgentMiddleware, + RequestTimestampMiddleware, + RequestTimezoneMiddleware + ) + .forRoutes('*'); + } +} diff --git a/src/common/request/middleware/timestamp/request.timestamp.middleware.ts b/src/common/request/middleware/timestamp/request.timestamp.middleware.ts new file mode 100644 index 0000000..7b6be30 --- /dev/null +++ b/src/common/request/middleware/timestamp/request.timestamp.middleware.ts @@ -0,0 +1,25 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Response, NextFunction } from 'express'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable() +export class RequestTimestampMiddleware implements NestMiddleware { + constructor( + private readonly helperNumberService: HelperNumberService, + private readonly helperDateService: HelperDateService + ) {} + + async use( + req: IRequestApp, + res: Response, + next: NextFunction + ): Promise { + req.__xTimestamp = req['x-timestamp'] + ? this.helperNumberService.create(req['x-timestamp']) + : undefined; + req.__timestamp = this.helperDateService.timestamp(); + next(); + } +} diff --git a/src/common/request/middleware/timezone/request.timezone.middleware.ts b/src/common/request/middleware/timezone/request.timezone.middleware.ts new file mode 100644 index 0000000..408fb56 --- /dev/null +++ b/src/common/request/middleware/timezone/request.timezone.middleware.ts @@ -0,0 +1,15 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Response, NextFunction } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable() +export class RequestTimezoneMiddleware implements NestMiddleware { + async use( + req: IRequestApp, + res: Response, + next: NextFunction + ): Promise { + req.__timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + next(); + } +} diff --git a/src/common/request/middleware/user-agent/request.user-agent.middleware.ts b/src/common/request/middleware/user-agent/request.user-agent.middleware.ts new file mode 100644 index 0000000..4bce93b --- /dev/null +++ b/src/common/request/middleware/user-agent/request.user-agent.middleware.ts @@ -0,0 +1,19 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Response, NextFunction } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { UAParser, IResult } from 'ua-parser-js'; + +@Injectable() +export class RequestUserAgentMiddleware implements NestMiddleware { + async use( + req: IRequestApp, + res: Response, + next: NextFunction + ): Promise { + const parserUserAgent = new UAParser(req['User-Agent']); + const userAgent: IResult = parserUserAgent.getResult(); + + req.__userAgent = userAgent; + next(); + } +} diff --git a/src/common/request/middleware/version/request.version.middleware.ts b/src/common/request/middleware/version/request.version.middleware.ts new file mode 100644 index 0000000..ceee04c --- /dev/null +++ b/src/common/request/middleware/version/request.version.middleware.ts @@ -0,0 +1,53 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Response, NextFunction } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +@Injectable() +export class RequestVersionMiddleware implements NestMiddleware { + private readonly versioningEnable: boolean; + + private readonly versioningGlobalPrefix: string; + private readonly versioningPrefix: string; + private readonly versioningVersion: string; + + private readonly repoVersion: string; + + constructor(private readonly configService: ConfigService) { + this.versioningGlobalPrefix = + this.configService.get('app.globalPrefix'); + this.versioningEnable = this.configService.get( + 'app.versioning.enable' + ); + this.versioningPrefix = this.configService.get( + 'app.versioning.prefix' + ); + this.versioningVersion = this.configService.get( + 'app.versioning.version' + ); + this.repoVersion = this.configService.get('app.repoVersion'); + } + + async use( + req: IRequestApp, + res: Response, + next: NextFunction + ): Promise { + const originalUrl: string = req.originalUrl; + let version = this.versioningVersion; + if ( + this.versioningEnable && + originalUrl.startsWith( + `${this.versioningGlobalPrefix}/${this.versioningPrefix}` + ) + ) { + const url: string[] = originalUrl.split('/'); + version = url[2].replace(this.versioningPrefix, ''); + } + + req.__version = version; + req.__repoVersion = this.repoVersion; + + next(); + } +} diff --git a/src/common/request/request.module.ts b/src/common/request/request.module.ts new file mode 100644 index 0000000..6258684 --- /dev/null +++ b/src/common/request/request.module.ts @@ -0,0 +1,88 @@ +import { + HttpStatus, + Module, + UnprocessableEntityException, + ValidationError, + ValidationPipe, +} from '@nestjs/common'; +import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; +import { RequestTimeoutInterceptor } from 'src/common/request/interceptors/request.timeout.interceptor'; +import { RequestMiddlewareModule } from 'src/common/request/middleware/request.middleware.module'; +import { MaxDateTodayConstraint } from 'src/common/request/validations/request.max-date-today.validation'; +import { MinDateTodayConstraint } from 'src/common/request/validations/request.min-date-today.validation'; +import { MobileNumberAllowedConstraint } from 'src/common/request/validations/request.mobile-number-allowed.validation'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from './constants/request.status-code.constant'; +import { IsPasswordMediumConstraint } from './validations/request.is-password-medium.validation'; +import { IsPasswordStrongConstraint } from './validations/request.is-password-strong.validation'; +import { IsPasswordWeakConstraint } from './validations/request.is-password-weak.validation'; +import { IsStartWithConstraint } from './validations/request.is-start-with.validation'; +import { MaxGreaterThanEqualConstraint } from './validations/request.max-greater-than-equal.validation'; +import { MaxGreaterThanConstraint } from './validations/request.max-greater-than.validation'; +import { MinGreaterThanEqualConstraint } from './validations/request.min-greater-than-equal.validation'; +import { MinGreaterThanConstraint } from './validations/request.min-greater-than.validation'; +import { IsOnlyDigitsConstraint } from './validations/request.only-digits.validation'; +import { SafeStringConstraint } from './validations/request.safe-string.validation'; +import { SkipConstraint } from './validations/request.skip.validation'; +import { MaxBinaryFileConstraint } from 'src/common/request/validations/request.max-binary-file.validation'; +import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; +import { ConfigModule, ConfigService } from '@nestjs/config'; + +@Module({ + controllers: [], + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: RequestTimeoutInterceptor, + }, + { + provide: APP_PIPE, + useFactory: () => + new ValidationPipe({ + transform: true, + skipNullProperties: false, + skipUndefinedProperties: false, + skipMissingProperties: false, + forbidUnknownValues: false, + errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, + exceptionFactory: async (errors: ValidationError[]) => + new UnprocessableEntityException({ + statusCode: + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR, + message: 'request.validation', + errors, + }), + }), + }, + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + }, + IsPasswordStrongConstraint, + IsPasswordMediumConstraint, + IsPasswordWeakConstraint, + IsStartWithConstraint, + MaxGreaterThanEqualConstraint, + MaxGreaterThanConstraint, + MinGreaterThanEqualConstraint, + MinGreaterThanConstraint, + SkipConstraint, + SafeStringConstraint, + IsOnlyDigitsConstraint, + MinDateTodayConstraint, + MobileNumberAllowedConstraint, + MaxDateTodayConstraint, + MaxBinaryFileConstraint, + ], + imports: [ + RequestMiddlewareModule, + ThrottlerModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + ttl: config.get('request.throttle.ttl'), + limit: config.get('request.throttle.limit'), + }), + }), + ], +}) +export class RequestModule {} diff --git a/src/common/request/serializations/request.pagination.serialization.ts b/src/common/request/serializations/request.pagination.serialization.ts new file mode 100644 index 0000000..861afc5 --- /dev/null +++ b/src/common/request/serializations/request.pagination.serialization.ts @@ -0,0 +1,16 @@ +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; + +export class RequestPaginationSerialization { + search: string; + filters: Record< + string, + string | number | boolean | Array + >; + page: number; + perPage: number; + orderBy: string; + orderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE; + availableSearch: string[]; + availableOrderBy: string[]; + availableOrderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE[]; +} diff --git a/src/common/request/validations/request.is-password-medium.validation.ts b/src/common/request/validations/request.is-password-medium.validation.ts new file mode 100644 index 0000000..e6fa7bd --- /dev/null +++ b/src/common/request/validations/request.is-password-medium.validation.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class IsPasswordMediumConstraint + implements ValidatorConstraintInterface +{ + constructor(protected readonly helperStringService: HelperStringService) {} + + validate(value: string, args: ValidationArguments): boolean { + const [length] = args.constraints; + return value + ? this.helperStringService.checkPasswordMedium(value, length) + : false; + } +} + +export function IsPasswordMedium( + minLength = 8, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'IsPasswordMedium', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [minLength], + validator: IsPasswordMediumConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.is-password-strong.validation.ts b/src/common/request/validations/request.is-password-strong.validation.ts new file mode 100644 index 0000000..aafccfb --- /dev/null +++ b/src/common/request/validations/request.is-password-strong.validation.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class IsPasswordStrongConstraint + implements ValidatorConstraintInterface +{ + constructor(protected readonly helperStringService: HelperStringService) {} + + validate(value: string, args: ValidationArguments): boolean { + const [length] = args.constraints; + return value + ? this.helperStringService.checkPasswordStrong(value, length) + : false; + } +} + +export function IsPasswordStrong( + minLength = 8, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'IsPasswordStrong', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [minLength], + validator: IsPasswordStrongConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.is-password-weak.validation.ts b/src/common/request/validations/request.is-password-weak.validation.ts new file mode 100644 index 0000000..ea605ee --- /dev/null +++ b/src/common/request/validations/request.is-password-weak.validation.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class IsPasswordWeakConstraint implements ValidatorConstraintInterface { + constructor(protected readonly helperStringService: HelperStringService) {} + + validate(value: string, args: ValidationArguments): boolean { + const [length] = args.constraints; + return value + ? this.helperStringService.checkPasswordMedium(value, length) + : false; + } +} + +export function IsPasswordWeak( + minLength = 8, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'IsPasswordWeak', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [minLength], + validator: IsPasswordWeakConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.is-start-with.validation.ts b/src/common/request/validations/request.is-start-with.validation.ts new file mode 100644 index 0000000..28c6c97 --- /dev/null +++ b/src/common/request/validations/request.is-start-with.validation.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class IsStartWithConstraint implements ValidatorConstraintInterface { + validate(value: string, args: ValidationArguments): boolean { + const [prefix] = args.constraints; + const check = prefix.find((val: string) => value.startsWith(val)); + return check ?? false; + } +} + +export function IsStartWith( + prefix: string[], + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'IsStartWith', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [prefix], + validator: IsStartWithConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.max-binary-file.validation.ts b/src/common/request/validations/request.max-binary-file.validation.ts new file mode 100644 index 0000000..ee9fc30 --- /dev/null +++ b/src/common/request/validations/request.max-binary-file.validation.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { ENUM_FILE_TYPE } from 'src/common/file/constants/file.enum.constant'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MaxBinaryFileConstraint implements ValidatorConstraintInterface { + constructor(private readonly configService: ConfigService) {} + + validate(value: string, args: ValidationArguments): boolean { + const [type] = args.constraints; + let fileSize = 0; + + switch (type) { + case ENUM_FILE_TYPE.AUDIO: + fileSize = this.configService.get( + 'file.audio.maxFileSize' + ); + break; + case ENUM_FILE_TYPE.EXCEL: + fileSize = this.configService.get( + 'file.excel.maxFileSize' + ); + break; + case ENUM_FILE_TYPE.IMAGE: + fileSize = this.configService.get( + 'file.image.maxFileSize' + ); + break; + case ENUM_FILE_TYPE.VIDEO: + fileSize = this.configService.get( + 'file.video.maxFileSize' + ); + break; + default: + break; + } + + return fileSize <= value.length; + } +} + +export function MaxBinaryFile( + type: ENUM_FILE_TYPE, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): any { + registerDecorator({ + name: 'MaxBinaryFile', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [type], + validator: MaxBinaryFileConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.max-date-today.validation.ts b/src/common/request/validations/request.max-date-today.validation.ts new file mode 100644 index 0000000..77def22 --- /dev/null +++ b/src/common/request/validations/request.max-date-today.validation.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MaxDateTodayConstraint implements ValidatorConstraintInterface { + constructor(private readonly helperDateService: HelperDateService) {} + + validate(value: string): boolean { + const todayDate = this.helperDateService.endOfDay(); + const valueDate = this.helperDateService.create(value); + return valueDate <= todayDate; + } +} + +export function MaxDateToday(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string): any { + registerDecorator({ + name: 'MaxDateToday', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: MaxDateTodayConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.max-greater-than-equal.validation.ts b/src/common/request/validations/request.max-greater-than-equal.validation.ts new file mode 100644 index 0000000..10e1515 --- /dev/null +++ b/src/common/request/validations/request.max-greater-than-equal.validation.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MaxGreaterThanEqualConstraint + implements ValidatorConstraintInterface +{ + validate(value: string, args: ValidationArguments): boolean { + const [property] = args.constraints; + const relatedValue = args.object[property]; + return value <= relatedValue; + } +} + +export function MaxGreaterThanEqual( + property: string, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'MaxGreaterThanEqual', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [property], + validator: MaxGreaterThanEqualConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.max-greater-than.validation.ts b/src/common/request/validations/request.max-greater-than.validation.ts new file mode 100644 index 0000000..9a1f534 --- /dev/null +++ b/src/common/request/validations/request.max-greater-than.validation.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MaxGreaterThanConstraint implements ValidatorConstraintInterface { + validate(value: string, args: ValidationArguments): boolean { + const [property] = args.constraints; + const relatedValue = args.object[property]; + return value < relatedValue; + } +} + +export function MaxGreaterThan( + property: string, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'MaxGreaterThan', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [property], + validator: MaxGreaterThanConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.min-date-today.validation.ts b/src/common/request/validations/request.min-date-today.validation.ts new file mode 100644 index 0000000..4c7d7bf --- /dev/null +++ b/src/common/request/validations/request.min-date-today.validation.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MinDateTodayConstraint implements ValidatorConstraintInterface { + constructor(private readonly helperDateService: HelperDateService) {} + + validate(value: string): boolean { + const todayDate = this.helperDateService.startOfDay(); + const valueDate = this.helperDateService.create(value); + return valueDate >= todayDate; + } +} + +export function MinDateToday(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'MinDateTodayEqual', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: MinDateTodayConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.min-greater-than-equal.validation.ts b/src/common/request/validations/request.min-greater-than-equal.validation.ts new file mode 100644 index 0000000..0fd8f4e --- /dev/null +++ b/src/common/request/validations/request.min-greater-than-equal.validation.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MinGreaterThanEqualConstraint + implements ValidatorConstraintInterface +{ + validate(value: string, args: ValidationArguments): boolean { + const [property] = args.constraints; + const relatedValue = args.object[property]; + return value >= relatedValue; + } +} + +export function MinGreaterThanEqual( + property: string, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'MinGreaterThanEqual', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [property], + validator: MinGreaterThanEqualConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.min-greater-than.validation.ts b/src/common/request/validations/request.min-greater-than.validation.ts new file mode 100644 index 0000000..3a3e028 --- /dev/null +++ b/src/common/request/validations/request.min-greater-than.validation.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MinGreaterThanConstraint implements ValidatorConstraintInterface { + validate(value: string, args: ValidationArguments): boolean { + const [property] = args.constraints; + const relatedValue = args.object[property]; + return value > relatedValue; + } +} + +export function MinGreaterThan( + property: string, + validationOptions?: ValidationOptions +) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'MinGreaterThan', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [property], + validator: MinGreaterThanConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.mobile-number-allowed.validation.ts b/src/common/request/validations/request.mobile-number-allowed.validation.ts new file mode 100644 index 0000000..b33ee4d --- /dev/null +++ b/src/common/request/validations/request.mobile-number-allowed.validation.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { SettingService } from 'src/common/setting/services/setting.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class MobileNumberAllowedConstraint + implements ValidatorConstraintInterface +{ + constructor(private readonly settingService: SettingService) {} + + async validate(value: string): Promise { + const mobileNumbersSetting: string[] = + await this.settingService.getMobileNumberCountryCodeAllowed(); + mobileNumbersSetting; + const check = mobileNumbersSetting.find((val) => value.startsWith(val)); + + return !!check; + } +} + +export function MobileNumberAllowed(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'MobileNumberAllowed', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: MobileNumberAllowedConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.only-digits.validation.ts b/src/common/request/validations/request.only-digits.validation.ts new file mode 100644 index 0000000..382228f --- /dev/null +++ b/src/common/request/validations/request.only-digits.validation.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class IsOnlyDigitsConstraint implements ValidatorConstraintInterface { + constructor(protected readonly helperNumberService: HelperNumberService) {} + + validate(value: string): boolean { + return value ? this.helperNumberService.check(value) : false; + } +} + +export function IsOnlyDigits(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'IsOnlyDigits', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: IsOnlyDigitsConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.safe-string.validation.ts b/src/common/request/validations/request.safe-string.validation.ts new file mode 100644 index 0000000..a5ff9ab --- /dev/null +++ b/src/common/request/validations/request.safe-string.validation.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class SafeStringConstraint implements ValidatorConstraintInterface { + constructor(protected readonly helperStringService: HelperStringService) {} + + validate(value: string): boolean { + return value ? this.helperStringService.checkSafeString(value) : false; + } +} + +export function SafeString(validationOptions?: ValidationOptions) { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'SafeString', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: SafeStringConstraint, + }); + }; +} diff --git a/src/common/request/validations/request.skip.validation.ts b/src/common/request/validations/request.skip.validation.ts new file mode 100644 index 0000000..f4f2e65 --- /dev/null +++ b/src/common/request/validations/request.skip.validation.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { + registerDecorator, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: true }) +@Injectable() +export class SkipConstraint implements ValidatorConstraintInterface { + validate(): boolean { + return true; + } +} + +export function Skip() { + return function (object: Record, propertyName: string): void { + registerDecorator({ + name: 'Skip', + target: object.constructor, + propertyName: propertyName, + validator: SkipConstraint, + }); + }; +} diff --git a/src/common/response/constants/response.constant.ts b/src/common/response/constants/response.constant.ts new file mode 100644 index 0000000..d5d1f2e --- /dev/null +++ b/src/common/response/constants/response.constant.ts @@ -0,0 +1,7 @@ +export const RESPONSE_SERIALIZATION_META_KEY = 'ResponseSerializationMetaKey'; +export const RESPONSE_SERIALIZATION_OPTIONS_META_KEY = + 'class_serializer:options'; +export const RESPONSE_MESSAGE_PROPERTIES_META_KEY = + 'ResponseSerializationPropertiesMetaKey'; +export const RESPONSE_MESSAGE_PATH_META_KEY = 'ResponseMessagePathMetaKey'; +export const RESPONSE_EXCEL_TYPE_META_KEY = 'ResponseExcelTypeMetaKey'; diff --git a/src/common/response/decorators/response.decorator.ts b/src/common/response/decorators/response.decorator.ts new file mode 100644 index 0000000..203b83e --- /dev/null +++ b/src/common/response/decorators/response.decorator.ts @@ -0,0 +1,79 @@ +import { + applyDecorators, + SerializeOptions, + SetMetadata, + UseInterceptors, +} from '@nestjs/common'; +import { ENUM_HELPER_FILE_TYPE } from 'src/common/helper/constants/helper.enum.constant'; +import { + RESPONSE_EXCEL_TYPE_META_KEY, + RESPONSE_MESSAGE_PATH_META_KEY, + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + RESPONSE_SERIALIZATION_META_KEY, +} from 'src/common/response/constants/response.constant'; +import { ResponseDefaultInterceptor } from 'src/common/response/interceptors/response.default.interceptor'; +import { ResponseExcelInterceptor } from 'src/common/response/interceptors/response.excel.interceptor'; +import { ResponsePagingInterceptor } from 'src/common/response/interceptors/response.paging.interceptor'; +import { + IResponseOptions, + IResponsePagingOptions, + IResponseExcelOptions, +} from 'src/common/response/interfaces/response.interface'; + +export function Response( + messagePath: string, + options?: IResponseOptions +): MethodDecorator { + return applyDecorators( + UseInterceptors(ResponseDefaultInterceptor), + SetMetadata(RESPONSE_MESSAGE_PATH_META_KEY, messagePath), + SetMetadata( + RESPONSE_SERIALIZATION_META_KEY, + options ? options.serialization : undefined + ), + SetMetadata( + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + options ? options.messageProperties : undefined + ) + ); +} + +export function ResponseExcel( + options?: IResponseExcelOptions +): MethodDecorator { + return applyDecorators( + UseInterceptors(ResponseExcelInterceptor), + SetMetadata( + RESPONSE_SERIALIZATION_META_KEY, + options ? options.serialization : undefined + ), + SetMetadata( + RESPONSE_EXCEL_TYPE_META_KEY, + options ? options.fileType : ENUM_HELPER_FILE_TYPE.CSV + ), + SetMetadata( + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + options ? options.messageProperties : undefined + ) + ); +} + +export function ResponsePaging( + messagePath: string, + options?: IResponsePagingOptions +): MethodDecorator { + return applyDecorators( + UseInterceptors(ResponsePagingInterceptor), + SetMetadata(RESPONSE_MESSAGE_PATH_META_KEY, messagePath), + SetMetadata( + RESPONSE_SERIALIZATION_META_KEY, + options ? options.serialization : undefined + ), + SetMetadata( + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + options ? options.messageProperties : undefined + ) + ); +} + +export const ResponseSerializationOptions = SerializeOptions; diff --git a/src/common/response/interceptors/response.custom-headers.interceptor.ts b/src/common/response/interceptors/response.custom-headers.interceptor.ts new file mode 100644 index 0000000..4f39abd --- /dev/null +++ b/src/common/response/interceptors/response.custom-headers.interceptor.ts @@ -0,0 +1,41 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { Response } from 'express'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; + +// only for response success and error in controller +@Injectable() +export class ResponseCustomHeadersInterceptor + implements NestInterceptor> +{ + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise | string>> { + if (context.getType() === 'http') { + const ctx: HttpArgumentsHost = context.switchToHttp(); + const responseExpress: Response = ctx.getResponse(); + const request: IRequestApp = ctx.getRequest(); + + responseExpress.setHeader('x-custom-lang', request.__customLang); + responseExpress.setHeader( + 'x-timestamp', + request.__xTimestamp ?? request.__timestamp + ); + responseExpress.setHeader('x-timezone', request.__timezone); + responseExpress.setHeader('x-request-id', request.__id); + responseExpress.setHeader('x-version', request.__version); + responseExpress.setHeader('x-repo-version', request.__repoVersion); + + return next.handle(); + } + + return next.handle(); + } +} diff --git a/src/common/response/interceptors/response.default.interceptor.ts b/src/common/response/interceptors/response.default.interceptor.ts new file mode 100644 index 0000000..6976984 --- /dev/null +++ b/src/common/response/interceptors/response.default.interceptor.ts @@ -0,0 +1,147 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { Response } from 'express'; +import { MessageService } from 'src/common/message/services/message.service'; +import { Reflector } from '@nestjs/core'; +import { + ClassConstructor, + ClassTransformOptions, + plainToInstance, +} from 'class-transformer'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { + IMessage, + IMessageOptionsProperties, +} from 'src/common/message/interfaces/message.interface'; +import { + ResponseDefaultSerialization, + ResponseMetadataSerialization, +} from 'src/common/response/serializations/response.default.serialization'; +import { + RESPONSE_MESSAGE_PATH_META_KEY, + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + RESPONSE_SERIALIZATION_META_KEY, + RESPONSE_SERIALIZATION_OPTIONS_META_KEY, +} from 'src/common/response/constants/response.constant'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; + +@Injectable() +export class ResponseDefaultInterceptor + implements NestInterceptor> +{ + constructor( + private readonly reflector: Reflector, + private readonly messageService: MessageService + ) {} + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise>> { + if (context.getType() === 'http') { + return next.handle().pipe( + map(async (res: Promise>) => { + const ctx: HttpArgumentsHost = context.switchToHttp(); + const response: Response = ctx.getResponse(); + const request: IRequestApp = ctx.getRequest(); + + let messagePath: string = this.reflector.get( + RESPONSE_MESSAGE_PATH_META_KEY, + context.getHandler() + ); + const classSerialization: ClassConstructor = + this.reflector.get>( + RESPONSE_SERIALIZATION_META_KEY, + context.getHandler() + ); + const classSerializationOptions: ClassTransformOptions = + this.reflector.get( + RESPONSE_SERIALIZATION_OPTIONS_META_KEY, + context.getHandler() + ); + let messageProperties: IMessageOptionsProperties = + this.reflector.get( + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + context.getHandler() + ); + + // metadata + const __customLang = request.__customLang; + const __requestId = request.__id; + const __path = request.path; + const __timestamp = + request.__xTimestamp ?? request.__timestamp; + const __timezone = request.__timezone; + const __version = request.__version; + const __repoVersion = request.__repoVersion; + + // set default response + let statusCode: number = response.statusCode; + let data: Record = undefined; + let metadata: ResponseMetadataSerialization = { + languages: __customLang, + timestamp: __timestamp, + timezone: __timezone, + requestId: __requestId, + path: __path, + version: __version, + repoVersion: __repoVersion, + }; + + // response + const responseData = (await res) as IResponse; + + if (responseData) { + const { _metadata } = responseData; + data = responseData.data; + + if (classSerialization) { + data = plainToInstance( + classSerialization, + data, + classSerializationOptions + ); + } + + statusCode = + _metadata?.customProperty?.statusCode ?? statusCode; + messagePath = + _metadata?.customProperty?.message ?? messagePath; + messageProperties = + _metadata?.customProperty?.messageProperties ?? + messageProperties; + + delete _metadata?.customProperty; + + metadata = { + ...metadata, + ..._metadata, + }; + } + + const message: string | IMessage = + await this.messageService.get(messagePath, { + customLanguages: __customLang, + properties: messageProperties, + }); + + return { + statusCode, + message, + _metadata: metadata, + data, + }; + }) + ); + } + + return next.handle(); + } +} diff --git a/src/common/response/interceptors/response.excel.interceptor.ts b/src/common/response/interceptors/response.excel.interceptor.ts new file mode 100644 index 0000000..450d8e8 --- /dev/null +++ b/src/common/response/interceptors/response.excel.interceptor.ts @@ -0,0 +1,105 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + StreamableFile, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { Response } from 'express'; +import { HelperFileService } from 'src/common/helper/services/helper.file.service'; +import { + ClassConstructor, + ClassTransformOptions, + plainToInstance, +} from 'class-transformer'; +import { Reflector } from '@nestjs/core'; +import { IResponseExcel } from 'src/common/response/interfaces/response.interface'; +import { + RESPONSE_EXCEL_TYPE_META_KEY, + RESPONSE_SERIALIZATION_META_KEY, + RESPONSE_SERIALIZATION_OPTIONS_META_KEY, +} from 'src/common/response/constants/response.constant'; +import { WorkBook } from 'xlsx'; +import { ENUM_HELPER_FILE_TYPE } from 'src/common/helper/constants/helper.enum.constant'; +import { ENUM_FILE_EXCEL_MIME } from 'src/common/file/constants/file.enum.constant'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; + +@Injectable() +export class ResponseExcelInterceptor implements NestInterceptor> { + constructor( + private readonly reflector: Reflector, + private readonly helperFileService: HelperFileService, + private readonly helperDateService: HelperDateService + ) {} + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise>>> { + const excelType: ENUM_HELPER_FILE_TYPE = + this.reflector.get( + RESPONSE_EXCEL_TYPE_META_KEY, + context.getHandler() + ); + + if (context.getType() === 'http') { + return next.handle().pipe( + map(async (res: Promise) => { + const ctx: HttpArgumentsHost = context.switchToHttp(); + const response: Response = ctx.getResponse(); + + const classSerialization: ClassConstructor = + this.reflector.get>( + RESPONSE_SERIALIZATION_META_KEY, + context.getHandler() + ); + const classSerializationOptions: ClassTransformOptions = + this.reflector.get( + RESPONSE_SERIALIZATION_OPTIONS_META_KEY, + context.getHandler() + ); + + // response + // set default response + const responseData = (await res) as IResponseExcel; + let data: Record[] = responseData.data; + if (classSerialization) { + data = plainToInstance( + classSerialization, + data, + classSerializationOptions + ); + } + + // create excel + const workbook: WorkBook = + this.helperFileService.createExcelWorkbook(data); + const excel: Buffer = + this.helperFileService.writeExcelToBuffer(workbook, { + type: excelType, + }); + + // set headers + const timestamp = this.helperDateService.timestamp(); + response + .setHeader( + 'Content-Type', + ENUM_FILE_EXCEL_MIME[excelType.toUpperCase()] + ) + .setHeader( + 'Content-Disposition', + `attachment; filename=export-${timestamp}.${excelType}` + ) + .setHeader('Content-Length', excel.length); + + return new StreamableFile(excel); + }) + ); + } + + return next.handle(); + } +} diff --git a/src/common/response/interceptors/response.paging.interceptor.ts b/src/common/response/interceptors/response.paging.interceptor.ts new file mode 100644 index 0000000..fc18b00 --- /dev/null +++ b/src/common/response/interceptors/response.paging.interceptor.ts @@ -0,0 +1,213 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { Response } from 'express'; +import { MessageService } from 'src/common/message/services/message.service'; +import { Reflector } from '@nestjs/core'; +import { + ClassConstructor, + ClassTransformOptions, + plainToInstance, +} from 'class-transformer'; +import qs from 'qs'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { + IMessage, + IMessageOptionsProperties, +} from 'src/common/message/interfaces/message.interface'; +import { + ResponsePagingCursorMetadataSerialization, + ResponsePagingMetadataSerialization, + ResponsePagingSerialization, +} from 'src/common/response/serializations/response.paging.serialization'; +import { + RESPONSE_MESSAGE_PATH_META_KEY, + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + RESPONSE_SERIALIZATION_META_KEY, + RESPONSE_SERIALIZATION_OPTIONS_META_KEY, +} from 'src/common/response/constants/response.constant'; +import { IResponsePaging } from 'src/common/response/interfaces/response.interface'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; + +@Injectable() +export class ResponsePagingInterceptor + implements NestInterceptor> +{ + constructor( + private readonly reflector: Reflector, + private readonly messageService: MessageService, + private readonly helperArrayService: HelperArrayService + ) {} + + async intercept( + context: ExecutionContext, + next: CallHandler + ): Promise>> { + if (context.getType() === 'http') { + return next.handle().pipe( + map(async (res: Promise) => { + const ctx: HttpArgumentsHost = context.switchToHttp(); + const response: Response = ctx.getResponse(); + const request: IRequestApp = ctx.getRequest(); + + let messagePath: string = this.reflector.get( + RESPONSE_MESSAGE_PATH_META_KEY, + context.getHandler() + ); + const classSerialization: ClassConstructor = + this.reflector.get>( + RESPONSE_SERIALIZATION_META_KEY, + context.getHandler() + ); + const classSerializationOptions: ClassTransformOptions = + this.reflector.get( + RESPONSE_SERIALIZATION_OPTIONS_META_KEY, + context.getHandler() + ); + let messageProperties: IMessageOptionsProperties = + this.reflector.get( + RESPONSE_MESSAGE_PROPERTIES_META_KEY, + context.getHandler() + ); + + // metadata + const __customLang = request.__customLang; + const __path = request.path; + const __requestId = request.__id; + const __timestamp = + request.__xTimestamp ?? request.__timestamp; + const __timezone = request.__timezone; + const __version = request.__version; + const __repoVersion = request.__repoVersion; + const __pagination = request.__pagination; + + let statusCode: number = response.statusCode; + let data: Record[] = []; + let metadata: ResponsePagingMetadataSerialization = { + languages: __customLang, + timestamp: __timestamp, + timezone: __timezone, + requestId: __requestId, + path: __path, + version: __version, + repoVersion: __repoVersion, + }; + + // response + const responseData = (await res) as IResponsePaging; + if (!responseData) { + throw new Error('Paging must have response'); + } + + const { _metadata } = responseData; + data = responseData.data; + + if (classSerialization) { + data = plainToInstance( + classSerialization, + data, + classSerializationOptions + ); + } + + statusCode = + _metadata?.customProperty?.statusCode ?? statusCode; + messagePath = + _metadata?.customProperty?.message ?? messagePath; + messageProperties = + _metadata?.customProperty?.messageProperties ?? + messageProperties; + + delete _metadata?.customProperty; + + // metadata pagination + + const { query } = request; + + delete query.perPage; + + delete query.page; + + const total: number = responseData._pagination.total; + + const totalPage: number = + responseData._pagination.totalPage; + + const perPage: number = __pagination.perPage; + const page: number = __pagination.page; + + const queryString = qs.stringify(query, { + encode: false, + }); + + const cursorPaginationMetadata: ResponsePagingCursorMetadataSerialization = + { + nextPage: + page < totalPage + ? `${__path}?perPage=${perPage}&page=${ + page + 1 + }&${queryString}` + : undefined, + previousPage: + page > 1 + ? `${__path}?perPage=${perPage}&page=${ + page - 1 + }&${queryString}` + : undefined, + firstPage: + totalPage > 1 + ? `${__path}?perPage=${perPage}&page=${1}&${queryString}` + : undefined, + lastPage: + totalPage > 1 + ? `${__path}?perPage=${perPage}&page=${totalPage}&${queryString}` + : undefined, + }; + + metadata = { + ...metadata, + ..._metadata, + pagination: { + ...__pagination, + ...metadata._pagination, + total, + totalPage, + }, + }; + + if ( + !this.helperArrayService.includes( + Object.values(cursorPaginationMetadata), + undefined + ) + ) { + metadata.cursor = cursorPaginationMetadata; + } + + const message: string | IMessage = + await this.messageService.get(messagePath, { + customLanguages: __customLang, + properties: messageProperties, + }); + + const responseHttp: ResponsePagingSerialization = { + statusCode, + message, + _metadata: metadata, + data, + }; + + return responseHttp; + }) + ); + } + + return next.handle(); + } +} diff --git a/src/common/response/interfaces/response.interface.ts b/src/common/response/interfaces/response.interface.ts new file mode 100644 index 0000000..ecf8325 --- /dev/null +++ b/src/common/response/interfaces/response.interface.ts @@ -0,0 +1,50 @@ +import { ClassConstructor } from 'class-transformer'; +import { ENUM_HELPER_FILE_TYPE } from 'src/common/helper/constants/helper.enum.constant'; +import { IHelperFileRows } from 'src/common/helper/interfaces/helper.interface'; +import { IMessageOptionsProperties } from 'src/common/message/interfaces/message.interface'; + +export interface IResponseCustomPropertyMetadata { + statusCode?: number; + message?: string; + messageProperties?: IMessageOptionsProperties; +} + +// metadata +export interface IResponseMetadata { + customProperty?: IResponseCustomPropertyMetadata; + [key: string]: any; +} + +// decorator options + +export interface IResponseOptions { + serialization?: ClassConstructor; + messageProperties?: IMessageOptionsProperties; +} + +export type IResponsePagingOptions = IResponseOptions; + +export interface IResponseExcelOptions extends IResponseOptions { + fileType?: ENUM_HELPER_FILE_TYPE; +} + +// type +export interface IResponse { + _metadata?: IResponseMetadata; + data: Record; +} + +export interface IResponsePagingPagination { + totalPage: number; + total: number; +} + +export interface IResponsePaging { + _metadata?: IResponseMetadata; + _pagination: IResponsePagingPagination; + data: Record[]; +} + +export interface IResponseExcel { + data: IHelperFileRows[]; +} diff --git a/src/common/response/middleware/response.middleware.module.ts b/src/common/response/middleware/response.middleware.module.ts new file mode 100644 index 0000000..638630b --- /dev/null +++ b/src/common/response/middleware/response.middleware.module.ts @@ -0,0 +1,9 @@ +import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; +import { ResponseTimeMiddleware } from 'src/common/response/middleware/time/response.time.middleware'; + +@Module({}) +export class ResponseMiddlewareModule implements NestModule { + configure(consumer: MiddlewareConsumer): void { + consumer.apply(ResponseTimeMiddleware).forRoutes('*'); + } +} diff --git a/src/common/response/middleware/time/response.time.middleware.ts b/src/common/response/middleware/time/response.time.middleware.ts new file mode 100644 index 0000000..185f579 --- /dev/null +++ b/src/common/response/middleware/time/response.time.middleware.ts @@ -0,0 +1,10 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import responseTime from 'response-time'; + +@Injectable() +export class ResponseTimeMiddleware implements NestMiddleware { + async use(req: Request, res: Response, next: NextFunction): Promise { + responseTime()(req, res, next); + } +} diff --git a/src/common/response/response.module.ts b/src/common/response/response.module.ts new file mode 100644 index 0000000..9db6dcd --- /dev/null +++ b/src/common/response/response.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { APP_INTERCEPTOR } from '@nestjs/core'; +import { ResponseMiddlewareModule } from 'src/common/response/middleware/response.middleware.module'; +import { ResponseCustomHeadersInterceptor } from './interceptors/response.custom-headers.interceptor'; + +@Module({ + controllers: [], + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: ResponseCustomHeadersInterceptor, + }, + ], + imports: [ResponseMiddlewareModule], +}) +export class ResponseModule {} diff --git a/src/common/response/serializations/response.default.serialization.ts b/src/common/response/serializations/response.default.serialization.ts new file mode 100644 index 0000000..d330536 --- /dev/null +++ b/src/common/response/serializations/response.default.serialization.ts @@ -0,0 +1,64 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IMessage } from 'src/common/message/interfaces/message.interface'; + +export class ResponseMetadataSerialization { + languages: string[]; + timestamp: number; + timezone: string; + requestId: string; + path: string; + version: string; + repoVersion: string; + [key: string]: any; +} + +export class ResponseDefaultSerialization> { + @ApiProperty({ + name: 'statusCode', + type: Number, + nullable: false, + description: 'return specific status code for every endpoints', + example: 200, + }) + statusCode: number; + + @ApiProperty({ + name: 'message', + nullable: false, + description: 'Message base on language', + oneOf: [ + { + type: 'string', + example: 'message endpoint', + }, + { + type: 'object', + example: { + en: 'This is test endpoint.', + id: 'Ini adalah endpoint test', + }, + }, + ], + }) + message: string | IMessage; + + @ApiProperty({ + name: '_metadata', + nullable: true, + description: 'Contain metadata about API', + type: 'object', + required: true, + example: { + languages: ['en'], + timestamp: 1660190937231, + timezone: 'Asia/Jakarta', + requestId: '40c2f734-7247-472b-bc26-8eff6e669781', + path: '/api/v1/test/hello', + version: '1', + repoVersion: '1.0.0', + }, + }) + _metadata?: ResponseMetadataSerialization; + + data?: T; +} diff --git a/src/common/response/serializations/response.id.serialization.ts b/src/common/response/serializations/response.id.serialization.ts new file mode 100644 index 0000000..b14ec7a --- /dev/null +++ b/src/common/response/serializations/response.id.serialization.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +export class ResponseIdSerialization { + @ApiProperty({ + description: 'Id that representative with your target data', + example: '631d9f32a65cf07250b8938c', + required: true, + }) + @Type(() => String) + _id: string; +} diff --git a/src/common/response/serializations/response.paging.serialization.ts b/src/common/response/serializations/response.paging.serialization.ts new file mode 100644 index 0000000..9dc86cf --- /dev/null +++ b/src/common/response/serializations/response.paging.serialization.ts @@ -0,0 +1,72 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { PAGINATION_AVAILABLE_ORDER_DIRECTION } from 'src/common/pagination/constants/pagination.constant'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { RequestPaginationSerialization } from 'src/common/request/serializations/request.pagination.serialization'; +import { + ResponseDefaultSerialization, + ResponseMetadataSerialization, +} from 'src/common/response/serializations/response.default.serialization'; + +export class ResponsePagingCursorMetadataSerialization { + nextPage: string; + previousPage: string; + firstPage: string; + lastPage: string; +} + +export class ResponsePagingPaginationSerialization extends RequestPaginationSerialization { + total: number; + totalPage: number; +} + +export interface ResponsePagingMetadataSerialization + extends ResponseMetadataSerialization { + cursor?: ResponsePagingCursorMetadataSerialization; + pagination?: ResponsePagingPaginationSerialization; +} + +export class ResponsePagingSerialization< + T = Record +> extends PickType(ResponseDefaultSerialization, [ + 'statusCode', + 'message', +] as const) { + @ApiProperty({ + name: '_metadata', + nullable: false, + description: 'Contain metadata about API', + type: 'object', + required: true, + example: { + languages: ['en'], + timestamp: 1660190937231, + timezone: 'Asia/Jakarta', + requestId: '40c2f734-7247-472b-bc26-8eff6e669781', + path: '/api/v1/test/hello', + version: '1', + repoVersion: '1.0.0', + pagination: { + search: faker.name.firstName(), + page: 1, + perPage: 20, + orderBy: 'createdAt', + orderDirection: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC, + availableSearch: ['name'], + availableOrderBy: ['createdAt'], + availableOrderDirection: PAGINATION_AVAILABLE_ORDER_DIRECTION, + total: 100, + totalPage: 5, + }, + cursor: { + nextPage: `http://217.0.0.1/__path?perPage=10&page=3&search=abc`, + previousPage: `http://217.0.0.1/__path?perPage=10&page=1&search=abc`, + firstPage: `http://217.0.0.1/__path?perPage=10&page=1&search=abc`, + lastPage: `http://217.0.0.1/__path?perPage=10&page=20&search=abc`, + }, + }, + }) + readonly _metadata: ResponsePagingMetadataSerialization; + + readonly data: T[]; +} diff --git a/src/common/setting/constants/setting.doc.constant.ts b/src/common/setting/constants/setting.doc.constant.ts new file mode 100644 index 0000000..16108da --- /dev/null +++ b/src/common/setting/constants/setting.doc.constant.ts @@ -0,0 +1,19 @@ +export const SettingDocParamsGet = [ + { + name: 'setting', + allowEmptyValue: false, + required: true, + type: 'string', + description: 'setting id', + }, +]; + +export const SettingDocParamsGetByName = [ + { + name: 'settingName', + allowEmptyValue: false, + required: true, + type: 'string', + description: 'setting name', + }, +]; diff --git a/src/common/setting/constants/setting.enum.constant.ts b/src/common/setting/constants/setting.enum.constant.ts new file mode 100644 index 0000000..84c8a38 --- /dev/null +++ b/src/common/setting/constants/setting.enum.constant.ts @@ -0,0 +1,6 @@ +export enum ENUM_SETTING_DATA_TYPE { + BOOLEAN = 'BOOLEAN', + STRING = 'STRING', + ARRAY_OF_STRING = 'ARRAY_OF_STRING', + NUMBER = 'NUMBER', +} diff --git a/src/common/setting/constants/setting.list.constant.ts b/src/common/setting/constants/setting.list.constant.ts new file mode 100644 index 0000000..548ad7c --- /dev/null +++ b/src/common/setting/constants/setting.list.constant.ts @@ -0,0 +1,8 @@ +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; + +export const SETTING_DEFAULT_PER_PAGE = 20; +export const SETTING_DEFAULT_ORDER_BY = 'createdAt'; +export const SETTING_DEFAULT_ORDER_DIRECTION = + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC; +export const SETTING_DEFAULT_AVAILABLE_SEARCH = ['name']; +export const SETTING_DEFAULT_AVAILABLE_ORDER_BY = ['name', 'createdAt']; diff --git a/src/common/setting/constants/setting.status-code.constant.ts b/src/common/setting/constants/setting.status-code.constant.ts new file mode 100644 index 0000000..ea07036 --- /dev/null +++ b/src/common/setting/constants/setting.status-code.constant.ts @@ -0,0 +1,4 @@ +export enum ENUM_SETTING_STATUS_CODE_ERROR { + SETTING_NOT_FOUND_ERROR = 5900, + SETTING_VALUE_NOT_ALLOWED_ERROR = 5901, +} diff --git a/src/common/setting/controllers/setting.admin.controller.ts b/src/common/setting/controllers/setting.admin.controller.ts new file mode 100644 index 0000000..0003d9e --- /dev/null +++ b/src/common/setting/controllers/setting.admin.controller.ts @@ -0,0 +1,77 @@ +import { + BadRequestException, + Body, + Controller, + InternalServerErrorException, + Put, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { AuthJwtAdminAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator'; +import { AuthPermissionProtected } from 'src/common/auth/decorators/auth.permission.decorator'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { RequestParamGuard } from 'src/common/request/decorators/request.decorator'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { ENUM_SETTING_STATUS_CODE_ERROR } from 'src/common/setting/constants/setting.status-code.constant'; +import { SettingUpdateGuard } from 'src/common/setting/decorators/setting.admin.decorator'; +import { GetSetting } from 'src/common/setting/decorators/setting.decorator'; +import { SettingUpdateDoc } from 'src/common/setting/docs/setting.admin.doc'; +import { SettingRequestDto } from 'src/common/setting/dtos/setting.request.dto'; +import { SettingUpdateValueDto } from 'src/common/setting/dtos/setting.update-value.dto'; +import { SettingDoc } from 'src/common/setting/repository/entities/setting.entity'; +import { SettingService } from 'src/common/setting/services/setting.service'; + +@ApiTags('admin.setting') +@Controller({ + version: '1', + path: '/setting', +}) +export class SettingAdminController { + constructor(private readonly settingService: SettingService) {} + + @SettingUpdateDoc() + @Response('setting.update', { + serialization: ResponseIdSerialization, + }) + @SettingUpdateGuard() + @RequestParamGuard(SettingRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.SETTING_READ, + ENUM_AUTH_PERMISSIONS.SETTING_UPDATE + ) + @AuthJwtAdminAccessProtected() + @Put('/update/:setting') + async update( + @GetSetting() setting: SettingDoc, + @Body() + body: SettingUpdateValueDto + ): Promise { + const check = await this.settingService.checkValue( + body.value, + body.type + ); + if (!check) { + throw new BadRequestException({ + statusCode: + ENUM_SETTING_STATUS_CODE_ERROR.SETTING_VALUE_NOT_ALLOWED_ERROR, + message: 'setting.error.valueNotAllowed', + }); + } + + try { + await this.settingService.updateValue(setting, body); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return { + data: { _id: setting._id }, + }; + } +} diff --git a/src/common/setting/controllers/setting.controller.ts b/src/common/setting/controllers/setting.controller.ts new file mode 100644 index 0000000..1e262ac --- /dev/null +++ b/src/common/setting/controllers/setting.controller.ts @@ -0,0 +1,112 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { PaginationQuery } from 'src/common/pagination/decorators/pagination.decorator'; +import { PaginationListDto } from 'src/common/pagination/dtos/pagination.list.dto'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { RequestParamGuard } from 'src/common/request/decorators/request.decorator'; +import { + Response, + ResponsePaging, +} from 'src/common/response/decorators/response.decorator'; +import { + IResponse, + IResponsePaging, +} from 'src/common/response/interfaces/response.interface'; +import { + SETTING_DEFAULT_AVAILABLE_ORDER_BY, + SETTING_DEFAULT_AVAILABLE_SEARCH, + SETTING_DEFAULT_ORDER_BY, + SETTING_DEFAULT_ORDER_DIRECTION, + SETTING_DEFAULT_PER_PAGE, +} from 'src/common/setting/constants/setting.list.constant'; +import { + GetSetting, + SettingGetByNameGuard, + SettingGetGuard, +} from 'src/common/setting/decorators/setting.decorator'; +import { + SettingGetByNameDoc, + SettingGetDoc, + SettingListDoc, +} from 'src/common/setting/docs/setting.doc'; +import { SettingRequestDto } from 'src/common/setting/dtos/setting.request.dto'; +import { SettingEntity } from 'src/common/setting/repository/entities/setting.entity'; +import { SettingGetSerialization } from 'src/common/setting/serializations/setting.get.serialization'; +import { SettingListSerialization } from 'src/common/setting/serializations/setting.list.serialization'; +import { SettingService } from 'src/common/setting/services/setting.service'; + +@ApiTags('setting') +@Controller({ + version: '1', + path: '/setting', +}) +export class SettingController { + constructor( + private readonly settingService: SettingService, + private readonly paginationService: PaginationService + ) {} + + @SettingListDoc() + @ResponsePaging('setting.list', { + serialization: SettingListSerialization, + }) + @Get('/list') + async list( + @PaginationQuery( + SETTING_DEFAULT_PER_PAGE, + SETTING_DEFAULT_ORDER_BY, + SETTING_DEFAULT_ORDER_DIRECTION, + SETTING_DEFAULT_AVAILABLE_SEARCH, + SETTING_DEFAULT_AVAILABLE_ORDER_BY + ) + { _search, _limit, _offset, _order }: PaginationListDto + ): Promise { + const find: Record = { + ..._search, + }; + + const settings: SettingEntity[] = await this.settingService.findAll( + find, + { + paging: { + limit: _limit, + offset: _offset, + }, + order: _order, + } + ); + const total: number = await this.settingService.getTotal(find); + const totalPage: number = this.paginationService.totalPage( + total, + _limit + ); + + return { + _pagination: { total, totalPage }, + data: settings, + }; + } + + @SettingGetDoc() + @Response('setting.get', { + serialization: SettingGetSerialization, + }) + @SettingGetGuard() + @RequestParamGuard(SettingRequestDto) + @Get('get/:setting') + async get(@GetSetting(true) setting: SettingEntity): Promise { + return { data: setting }; + } + + @SettingGetByNameDoc() + @Response('setting.getByName', { + serialization: SettingGetSerialization, + }) + @SettingGetByNameGuard() + @Get('get/name/:settingName') + async getByName( + @GetSetting(true) setting: SettingEntity + ): Promise { + return { data: setting }; + } +} diff --git a/src/common/setting/decorators/setting.admin.decorator.ts b/src/common/setting/decorators/setting.admin.decorator.ts new file mode 100644 index 0000000..e50523d --- /dev/null +++ b/src/common/setting/decorators/setting.admin.decorator.ts @@ -0,0 +1,9 @@ +import { applyDecorators, UseGuards } from '@nestjs/common'; +import { SettingNotFoundGuard } from 'src/common/setting/guards/setting.not-found.guard'; +import { SettingPutToRequestGuard } from 'src/common/setting/guards/setting.put-to-request.guard'; + +export function SettingUpdateGuard(): MethodDecorator { + return applyDecorators( + UseGuards(SettingPutToRequestGuard, SettingNotFoundGuard) + ); +} diff --git a/src/common/setting/decorators/setting.decorator.ts b/src/common/setting/decorators/setting.decorator.ts new file mode 100644 index 0000000..a5fa48b --- /dev/null +++ b/src/common/setting/decorators/setting.decorator.ts @@ -0,0 +1,31 @@ +import { + applyDecorators, + createParamDecorator, + ExecutionContext, + UseGuards, +} from '@nestjs/common'; +import { SettingNotFoundGuard } from 'src/common/setting/guards/setting.not-found.guard'; +import { + SettingPutToRequestByNameGuard, + SettingPutToRequestGuard, +} from 'src/common/setting/guards/setting.put-to-request.guard'; +import { SettingDoc } from 'src/common/setting/repository/entities/setting.entity'; + +export const GetSetting = createParamDecorator( + (returnPlain: boolean, ctx: ExecutionContext): SettingDoc => { + const { __setting } = ctx.switchToHttp().getRequest(); + return returnPlain ? __setting.toObject() : __setting; + } +); + +export function SettingGetGuard(): MethodDecorator { + return applyDecorators( + UseGuards(SettingPutToRequestGuard, SettingNotFoundGuard) + ); +} + +export function SettingGetByNameGuard(): MethodDecorator { + return applyDecorators( + UseGuards(SettingPutToRequestByNameGuard, SettingNotFoundGuard) + ); +} diff --git a/src/common/setting/docs/setting.admin.doc.ts b/src/common/setting/docs/setting.admin.doc.ts new file mode 100644 index 0000000..fa2e6d6 --- /dev/null +++ b/src/common/setting/docs/setting.admin.doc.ts @@ -0,0 +1,19 @@ +import { applyDecorators } from '@nestjs/common'; +import { Doc } from 'src/common/doc/decorators/doc.decorator'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { SettingDocParamsGet } from 'src/common/setting/constants/setting.doc.constant'; + +export function SettingUpdateDoc(): MethodDecorator { + return applyDecorators( + Doc('setting.update', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: SettingDocParamsGet, + }, + response: { serialization: ResponseIdSerialization }, + }) + ); +} diff --git a/src/common/setting/docs/setting.doc.ts b/src/common/setting/docs/setting.doc.ts new file mode 100644 index 0000000..5668161 --- /dev/null +++ b/src/common/setting/docs/setting.doc.ts @@ -0,0 +1,49 @@ +import { applyDecorators } from '@nestjs/common'; +import { Doc, DocPaging } from 'src/common/doc/decorators/doc.decorator'; +import { + SettingDocParamsGet, + SettingDocParamsGetByName, +} from 'src/common/setting/constants/setting.doc.constant'; +import { SettingGetSerialization } from 'src/common/setting/serializations/setting.get.serialization'; +import { SettingListSerialization } from 'src/common/setting/serializations/setting.list.serialization'; + +export function SettingListDoc(): MethodDecorator { + return applyDecorators( + DocPaging('setting.list', { + auth: { + jwtAccessToken: false, + }, + response: { + serialization: SettingListSerialization, + }, + }) + ); +} + +export function SettingGetByNameDoc(): MethodDecorator { + return applyDecorators( + Doc('setting.getByName', { + auth: { + jwtAccessToken: false, + }, + request: { + params: SettingDocParamsGetByName, + }, + response: { serialization: SettingGetSerialization }, + }) + ); +} + +export function SettingGetDoc(): MethodDecorator { + return applyDecorators( + Doc('setting.get', { + auth: { + jwtAccessToken: false, + }, + request: { + params: SettingDocParamsGet, + }, + response: { serialization: SettingGetSerialization }, + }) + ); +} diff --git a/src/common/setting/dtos/setting.create.dto.ts b/src/common/setting/dtos/setting.create.dto.ts new file mode 100644 index 0000000..db51bef --- /dev/null +++ b/src/common/setting/dtos/setting.create.dto.ts @@ -0,0 +1,48 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsString, IsNotEmpty, IsOptional } from 'class-validator'; +import { SafeString } from 'src/common/request/validations/request.safe-string.validation'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; + +export class SettingCreateDto { + @IsString() + @IsNotEmpty() + @SafeString() + @Type(() => String) + readonly name: string; + + @IsString() + @IsOptional() + @Type(() => String) + @ApiProperty({ + name: 'description', + examples: ['Maintenance Mode', 'Max Part Number Aws Chunk File'], + description: 'The description about setting', + nullable: true, + }) + readonly description?: string; + + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'Data type of setting', + example: 'BOOLEAN', + required: true, + enum: ENUM_SETTING_DATA_TYPE, + }) + readonly type: ENUM_SETTING_DATA_TYPE; + + @IsNotEmpty() + @Type(() => String) + @ApiProperty({ + name: 'value', + description: 'The value of setting', + nullable: false, + oneOf: [ + { type: 'string', readOnly: true, examples: ['on', 'off'] }, + { type: 'number', readOnly: true, examples: [100, 200] }, + { type: 'boolean', readOnly: true, examples: [true, false] }, + ], + }) + readonly value: string; +} diff --git a/src/common/setting/dtos/setting.request.dto.ts b/src/common/setting/dtos/setting.request.dto.ts new file mode 100644 index 0000000..affc2b9 --- /dev/null +++ b/src/common/setting/dtos/setting.request.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class SettingRequestDto { + @ApiProperty({ + name: 'setting', + description: 'setting id', + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsUUID('4') + @Type(() => String) + setting: string; +} diff --git a/src/common/setting/dtos/setting.update-value.dto.ts b/src/common/setting/dtos/setting.update-value.dto.ts new file mode 100644 index 0000000..537f92e --- /dev/null +++ b/src/common/setting/dtos/setting.update-value.dto.ts @@ -0,0 +1,6 @@ +import { OmitType } from '@nestjs/swagger'; +import { SettingCreateDto } from './setting.create.dto'; + +export class SettingUpdateValueDto extends OmitType(SettingCreateDto, [ + 'name', +] as const) {} diff --git a/src/common/setting/guards/setting.not-found.guard.ts b/src/common/setting/guards/setting.not-found.guard.ts new file mode 100644 index 0000000..f520db9 --- /dev/null +++ b/src/common/setting/guards/setting.not-found.guard.ts @@ -0,0 +1,24 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + NotFoundException, +} from '@nestjs/common'; +import { ENUM_SETTING_STATUS_CODE_ERROR } from 'src/common/setting/constants/setting.status-code.constant'; + +@Injectable() +export class SettingNotFoundGuard implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + const { __setting } = context.switchToHttp().getRequest(); + + if (!__setting) { + throw new NotFoundException({ + statusCode: + ENUM_SETTING_STATUS_CODE_ERROR.SETTING_NOT_FOUND_ERROR, + message: 'setting.error.notFound', + }); + } + + return true; + } +} diff --git a/src/common/setting/guards/setting.put-to-request.guard.ts b/src/common/setting/guards/setting.put-to-request.guard.ts new file mode 100644 index 0000000..72e7071 --- /dev/null +++ b/src/common/setting/guards/setting.put-to-request.guard.ts @@ -0,0 +1,39 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { SettingDoc } from 'src/common/setting/repository/entities/setting.entity'; +import { SettingService } from 'src/common/setting/services/setting.service'; + +@Injectable() +export class SettingPutToRequestGuard implements CanActivate { + constructor(private readonly settingService: SettingService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { params } = request; + const { setting } = params; + + const check: SettingDoc = await this.settingService.findOneById( + setting + ); + request.__setting = check; + + return true; + } +} + +@Injectable() +export class SettingPutToRequestByNameGuard implements CanActivate { + constructor(private readonly settingService: SettingService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { params } = request; + const { settingName } = params; + + const check: SettingDoc = await this.settingService.findOneByName( + settingName + ); + request.__setting = check; + + return true; + } +} diff --git a/src/common/setting/interfaces/setting.service.interface.ts b/src/common/setting/interfaces/setting.service.interface.ts new file mode 100644 index 0000000..38da1a9 --- /dev/null +++ b/src/common/setting/interfaces/setting.service.interface.ts @@ -0,0 +1,66 @@ +import { + IDatabaseCreateOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; +import { SettingCreateDto } from 'src/common/setting/dtos/setting.create.dto'; +import { SettingUpdateValueDto } from 'src/common/setting/dtos/setting.update-value.dto'; +import { + SettingDoc, + SettingEntity, +} from 'src/common/setting/repository/entities/setting.entity'; + +export interface ISettingService { + findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + + findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise; + + findOneByName( + name: string, + options?: IDatabaseFindOneOptions + ): Promise; + + getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise; + + create( + { name, description, type, value }: SettingCreateDto, + options?: IDatabaseCreateOptions + ): Promise; + + updateValue( + repository: SettingDoc, + { description, type, value }: SettingUpdateValueDto, + options?: IDatabaseOptions + ): Promise; + + delete(repository: SettingDoc): Promise; + + getValue(setting: SettingDoc): Promise; + + checkValue(value: string, type: ENUM_SETTING_DATA_TYPE): Promise; + + getMaintenance(): Promise; + + getMobileNumberCountryCodeAllowed(): Promise; + + getPasswordAttempt(): Promise; + + getMaxPasswordAttempt(): Promise; + + deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise; +} diff --git a/src/common/setting/middleware/maintenance/setting.maintenance.middleware.ts b/src/common/setting/middleware/maintenance/setting.maintenance.middleware.ts new file mode 100644 index 0000000..ae5d734 --- /dev/null +++ b/src/common/setting/middleware/maintenance/setting.maintenance.middleware.ts @@ -0,0 +1,32 @@ +import { + Injectable, + NestMiddleware, + ServiceUnavailableException, +} from '@nestjs/common'; +import { Response, NextFunction } from 'express'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { IRequestApp } from 'src/common/request/interfaces/request.interface'; +import { SettingService } from 'src/common/setting/services/setting.service'; + +@Injectable() +export class SettingMaintenanceMiddleware implements NestMiddleware { + constructor(private readonly settingService: SettingService) {} + + async use( + req: IRequestApp, + res: Response, + next: NextFunction + ): Promise { + const maintenance: boolean = await this.settingService.getMaintenance(); + + if (maintenance) { + throw new ServiceUnavailableException({ + statusCode: + ENUM_ERROR_STATUS_CODE_ERROR.ERROR_SERVICE_UNAVAILABLE, + message: 'http.serverError.serviceUnavailable', + }); + } + + next(); + } +} diff --git a/src/common/setting/middleware/setting.middleware.module.ts b/src/common/setting/middleware/setting.middleware.module.ts new file mode 100644 index 0000000..fa5a436 --- /dev/null +++ b/src/common/setting/middleware/setting.middleware.module.ts @@ -0,0 +1,50 @@ +import { + Module, + NestModule, + MiddlewareConsumer, + RequestMethod, +} from '@nestjs/common'; +import { SettingMaintenanceMiddleware } from 'src/common/setting/middleware/maintenance/setting.maintenance.middleware'; + +@Module({}) +export class SettingMiddlewareModule implements NestModule { + configure(consumer: MiddlewareConsumer): void { + consumer + .apply(SettingMaintenanceMiddleware) + .exclude( + { + path: 'api/v:version*/user/login', + method: RequestMethod.POST, + }, + { + path: 'api/user/login', + method: RequestMethod.POST, + }, + { + path: 'api/v:version*/user/refresh', + method: RequestMethod.POST, + }, + { + path: 'api/user/refresh', + method: RequestMethod.POST, + }, + { + path: 'api/v:version*/admin/setting/(.*)', + method: RequestMethod.ALL, + }, + { + path: 'api/admin/setting/(.*)', + method: RequestMethod.ALL, + }, + { + path: 'api/v:version*/setting/(.*)', + method: RequestMethod.ALL, + }, + { + path: 'api/setting/(.*)', + method: RequestMethod.ALL, + } + ) + .forRoutes('*'); + } +} diff --git a/src/common/setting/repository/entities/setting.entity.ts b/src/common/setting/repository/entities/setting.entity.ts new file mode 100644 index 0000000..97c603e --- /dev/null +++ b/src/common/setting/repository/entities/setting.entity.ts @@ -0,0 +1,43 @@ +import { Prop, SchemaFactory } from '@nestjs/mongoose'; +import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntity } from 'src/common/database/decorators/database.decorator'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; +import { Document } from 'mongoose'; + +export const SettingDatabaseName = 'settings'; + +@DatabaseEntity({ collection: SettingDatabaseName }) +export class SettingEntity extends DatabaseMongoUUIDEntityAbstract { + @Prop({ + required: true, + index: true, + unique: true, + trim: true, + type: String, + }) + name: string; + + @Prop({ + required: false, + type: String, + }) + description?: string; + + @Prop({ + required: false, + type: String, + enum: ENUM_SETTING_DATA_TYPE, + }) + type: ENUM_SETTING_DATA_TYPE; + + @Prop({ + required: true, + trim: true, + type: String, + }) + value: string; +} + +export const SettingSchema = SchemaFactory.createForClass(SettingEntity); + +export type SettingDoc = SettingEntity & Document; diff --git a/src/common/setting/repository/repositories/setting.repository.ts b/src/common/setting/repository/repositories/setting.repository.ts new file mode 100644 index 0000000..fc8d9a8 --- /dev/null +++ b/src/common/setting/repository/repositories/setting.repository.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { Model } from 'mongoose'; +import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; +import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; +import { + SettingDoc, + SettingEntity, +} from 'src/common/setting/repository/entities/setting.entity'; + +@Injectable() +export class SettingRepository extends DatabaseMongoUUIDRepositoryAbstract< + SettingEntity, + SettingDoc +> { + constructor( + @DatabaseModel(SettingEntity.name) + private readonly settingModel: Model + ) { + super(settingModel); + } +} diff --git a/src/common/setting/repository/setting.repository.module.ts b/src/common/setting/repository/setting.repository.module.ts new file mode 100644 index 0000000..15f9db0 --- /dev/null +++ b/src/common/setting/repository/setting.repository.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { + SettingEntity, + SettingSchema, +} from 'src/common/setting/repository/entities/setting.entity'; +import { SettingRepository } from 'src/common/setting/repository/repositories/setting.repository'; + +@Module({ + providers: [SettingRepository], + exports: [SettingRepository], + controllers: [], + imports: [ + MongooseModule.forFeature( + [ + { + name: SettingEntity.name, + schema: SettingSchema, + }, + ], + DATABASE_CONNECTION_NAME + ), + ], +}) +export class SettingRepositoryModule {} diff --git a/src/common/setting/serializations/setting.get.serialization.ts b/src/common/setting/serializations/setting.get.serialization.ts new file mode 100644 index 0000000..cf15f5d --- /dev/null +++ b/src/common/setting/serializations/setting.get.serialization.ts @@ -0,0 +1,74 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Exclude, Transform } from 'class-transformer'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; + +export class SettingGetSerialization extends ResponseIdSerialization { + @ApiProperty({ + description: 'Name of setting', + example: 'MaintenanceOn', + required: true, + }) + readonly name: string; + + @ApiProperty({ + description: 'Description of setting', + example: 'Maintenance Mode', + required: false, + }) + readonly description?: string; + + @ApiProperty({ + description: 'Data type of setting', + example: 'BOOLEAN', + required: true, + enum: ENUM_SETTING_DATA_TYPE, + }) + readonly type: ENUM_SETTING_DATA_TYPE; + + @ApiProperty({ + description: 'Value of string, can be type string/boolean/number', + oneOf: [ + { type: 'string', readOnly: true, examples: ['on', 'off'] }, + { type: 'number', readOnly: true, examples: [100, 200] }, + { type: 'boolean', readOnly: true, examples: [true, false] }, + ], + required: true, + }) + @Transform(({ value, obj }) => { + const regex = /^-?\d+$/; + const checkNum = regex.test(value); + + if ( + obj.type === ENUM_SETTING_DATA_TYPE.BOOLEAN && + (value === 'true' || value === 'false') + ) { + return value === 'true' ? true : false; + } else if (obj.type === ENUM_SETTING_DATA_TYPE.NUMBER && checkNum) { + return Number(value); + } else if (obj.type === ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING) { + return value.split(','); + } + + return value; + }) + readonly value: string | number | boolean; + + @ApiProperty({ + description: 'Date created at', + example: faker.date.recent(), + required: false, + }) + readonly createdAt: Date; + + @ApiProperty({ + description: 'Date updated at', + example: faker.date.recent(), + required: false, + }) + readonly updatedAt: Date; + + @Exclude() + readonly deletedAt?: Date; +} diff --git a/src/common/setting/serializations/setting.list.serialization.ts b/src/common/setting/serializations/setting.list.serialization.ts new file mode 100644 index 0000000..8ff828b --- /dev/null +++ b/src/common/setting/serializations/setting.list.serialization.ts @@ -0,0 +1,3 @@ +import { SettingGetSerialization } from './setting.get.serialization'; + +export class SettingListSerialization extends SettingGetSerialization {} diff --git a/src/common/setting/services/setting.service.ts b/src/common/setting/services/setting.service.ts new file mode 100644 index 0000000..873dc83 --- /dev/null +++ b/src/common/setting/services/setting.service.ts @@ -0,0 +1,170 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + IDatabaseCreateOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; +import { SettingCreateDto } from 'src/common/setting/dtos/setting.create.dto'; +import { SettingUpdateValueDto } from 'src/common/setting/dtos/setting.update-value.dto'; +import { ISettingService } from 'src/common/setting/interfaces/setting.service.interface'; +import { + SettingDoc, + SettingEntity, +} from 'src/common/setting/repository/entities/setting.entity'; +import { SettingRepository } from 'src/common/setting/repository/repositories/setting.repository'; + +@Injectable() +export class SettingService implements ISettingService { + private readonly mobileNumberCountryCodeAllowed: string[]; + private readonly passwordAttempt: boolean; + private readonly maxPasswordAttempt: number; + + constructor( + private readonly settingRepository: SettingRepository, + private readonly configService: ConfigService, + private readonly helperNumberService: HelperNumberService + ) { + this.mobileNumberCountryCodeAllowed = this.configService.get( + 'user.mobileNumberCountryCodeAllowed' + ); + this.passwordAttempt = this.configService.get( + 'auth.password.attempt' + ); + this.maxPasswordAttempt = this.configService.get( + 'auth.password.maxAttempt' + ); + } + + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.settingRepository.findAll(find, options); + } + + async findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.settingRepository.findOneById(_id, options); + } + + async findOneByName( + name: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.settingRepository.findOne( + { name }, options); + } + + async getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise { + return this.settingRepository.getTotal(find, options); + } + + async create( + { name, description, type, value }: SettingCreateDto, + options?: IDatabaseCreateOptions + ): Promise { + const create: SettingEntity = new SettingEntity(); + create.name = name; + create.description = description ?? undefined; + create.value = value; + create.type = type; + + return this.settingRepository.create(create, options); + } + + async updateValue( + repository: SettingDoc, + { description, type, value }: SettingUpdateValueDto + ): Promise { + repository.description = description; + repository.type = type; + repository.value = value; + + return this.settingRepository.save(repository); + } + + async delete(repository: SettingDoc): Promise { + return this.settingRepository.softDelete(repository); + } + + async getValue(setting: SettingDoc): Promise { + if ( + setting.type === ENUM_SETTING_DATA_TYPE.BOOLEAN && + (setting.value === 'true' || setting.value === 'false') + ) { + return (setting.value === 'true') as any; + } else if ( + setting.type === ENUM_SETTING_DATA_TYPE.NUMBER && + this.helperNumberService.check(setting.value) + ) { + return this.helperNumberService.create(setting.value) as any; + } else if (setting.type === ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING) { + return setting.value.split(',') as any; + } + + return setting.value as any; + } + + async checkValue( + value: string, + type: ENUM_SETTING_DATA_TYPE + ): Promise { + if ( + type === ENUM_SETTING_DATA_TYPE.BOOLEAN && + (value === 'true' || value === 'false') + ) { + return true; + } else if ( + type === ENUM_SETTING_DATA_TYPE.NUMBER && + this.helperNumberService.check(value) + ) { + return true; + } else if ( + (type === ENUM_SETTING_DATA_TYPE.STRING || + type === ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING) && + typeof value === 'string' + ) { + return true; + } + + return false; + } + + async getMaintenance(): Promise { + const setting: SettingDoc = + await this.settingRepository.findOne({ + name: 'maintenance', + }); + + return this.getValue(setting); + } + + async getMobileNumberCountryCodeAllowed(): Promise { + return this.mobileNumberCountryCodeAllowed; + } + + async getPasswordAttempt(): Promise { + return this.passwordAttempt; + } + + async getMaxPasswordAttempt(): Promise { + return this.maxPasswordAttempt; + } + + async deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise { + return this.settingRepository.deleteMany(find, options); + } +} diff --git a/src/common/setting/setting.module.ts b/src/common/setting/setting.module.ts new file mode 100644 index 0000000..55c2f0f --- /dev/null +++ b/src/common/setting/setting.module.ts @@ -0,0 +1,13 @@ +import { Global, Module } from '@nestjs/common'; +import { SettingMiddlewareModule } from 'src/common/setting/middleware/setting.middleware.module'; +import { SettingRepositoryModule } from 'src/common/setting/repository/setting.repository.module'; +import { SettingService } from './services/setting.service'; + +@Global() +@Module({ + imports: [SettingRepositoryModule, SettingMiddlewareModule], + exports: [SettingService], + providers: [SettingService], + controllers: [], +}) +export class SettingModule {} diff --git a/src/configs/app.config.ts b/src/configs/app.config.ts new file mode 100644 index 0000000..8289019 --- /dev/null +++ b/src/configs/app.config.ts @@ -0,0 +1,31 @@ +import { registerAs } from '@nestjs/config'; +import { version } from 'package.json'; +import { APP_LANGUAGE } from 'src/app/constants/app.constant'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant'; + +export default registerAs( + 'app', + (): Record => ({ + name: process.env.APP_NAME ?? 'nest', + env: process.env.APP_ENV ?? ENUM_APP_ENVIRONMENT.DEVELOPMENT, + language: process.env.APP_LANGUAGE?.split(',') ?? [APP_LANGUAGE], + + repoVersion: version, + versioning: { + enable: process.env.HTTP_VERSIONING_ENABLE === 'true' ?? false, + prefix: 'v', + version: process.env.HTTP_VERSION ?? '1', + }, + + globalPrefix: '/api', + http: { + enable: process.env.HTTP_ENABLE === 'true' ?? false, + host: process.env.HTTP_HOST ?? 'localhost', + port: process.env.HTTP_PORT + ? Number.parseInt(process.env.HTTP_PORT) + : 3000, + }, + + jobEnable: process.env.JOB_ENABLE === 'true' ?? false, + }) +); diff --git a/src/configs/auth.config.ts b/src/configs/auth.config.ts new file mode 100644 index 0000000..39c2665 --- /dev/null +++ b/src/configs/auth.config.ts @@ -0,0 +1,62 @@ +import { registerAs } from '@nestjs/config'; +import { seconds } from 'src/common/helper/constants/helper.function.constant'; + +export default registerAs( + 'auth', + (): Record => ({ + accessToken: { + secretKey: process.env.AUTH_JWT_ACCESS_TOKEN_SECRET_KEY ?? '123456', + expirationTime: seconds( + process.env.AUTH_JWT_ACCESS_TOKEN_EXPIRED ?? '15m' + ), // recommendation for production is 15m + notBeforeExpirationTime: seconds('0'), // keep it in zero value + + encryptKey: process.env.AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_KEY, + encryptIv: process.env.AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_IV, + }, + + refreshToken: { + secretKey: + process.env.AUTH_JWT_REFRESH_TOKEN_SECRET_KEY ?? '123456000', + expirationTime: seconds( + process.env.AUTH_JWT_REFRESH_TOKEN_EXPIRED ?? '7d' + ), // recommendation for production is 7d + expirationTimeRememberMe: seconds( + process.env.AUTH_JWT_REFRESH_TOKEN_REMEMBER_ME_EXPIRED ?? '30d' + ), // recommendation for production is 30d + notBeforeExpirationTime: seconds( + process.env.AUTH_JWT_REFRESH_TOKEN_NOT_BEFORE_EXPIRATION ?? + '15m' + ), // recommendation for production is 15m + + encryptKey: process.env.AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_KEY, + encryptIv: process.env.AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_IV, + }, + + subject: process.env.AUTH_JWT_SUBJECT ?? 'nestDevelopment', + audience: process.env.AUTH_JWT_AUDIENCE ?? 'https://example.com', + issuer: process.env.AUTH_JWT_ISSUER ?? 'nest', + prefixAuthorization: 'Bearer', + payloadEncryption: + process.env.AUTH_JWT_PAYLOAD_ENCRYPT === 'true' ? true : false, + + permissionToken: { + headerName: 'x-permission-token', + secretKey: process.env.AUTH_PERMISSION_TOKEN_SECRET_KEY ?? '123456', + expirationTime: seconds( + process.env.AUTH_PERMISSION_TOKEN_EXPIRED ?? '5m' + ), // recommendation for production is 5m + notBeforeExpirationTime: seconds('0'), // keep it in zero value + + encryptKey: process.env.AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_KEY, + encryptIv: process.env.AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_IV, + }, + + password: { + attempt: true, + maxAttempt: 3, + saltLength: 8, + expiredIn: seconds('182d'), // recommendation for production is 182 days + }, + }) +); diff --git a/src/configs/aws.config.ts b/src/configs/aws.config.ts new file mode 100644 index 0000000..6c52af2 --- /dev/null +++ b/src/configs/aws.config.ts @@ -0,0 +1,16 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'aws', + (): Record => ({ + credential: { + key: process.env.AWS_CREDENTIAL_KEY, + secret: process.env.AWS_CREDENTIAL_SECRET, + }, + s3: { + bucket: process.env.AWS_S3_BUCKET ?? 'nests3', + region: process.env.AWS_S3_REGION, + baseUrl: `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_S3_REGION}.amazonaws.com`, + }, + }) +); diff --git a/src/configs/database.config.ts b/src/configs/database.config.ts new file mode 100644 index 0000000..ee0fa8e --- /dev/null +++ b/src/configs/database.config.ts @@ -0,0 +1,13 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'database', + (): Record => ({ + host: process.env?.DATABASE_HOST ?? 'mongodb://localhost:27017', + name: process.env?.DATABASE_NAME ?? 'nest', + user: process.env?.DATABASE_USER, + password: process?.env.DATABASE_PASSWORD, + debug: process.env.DATABASE_DEBUG === 'true', + options: process.env?.DATABASE_OPTIONS, + }) +); diff --git a/src/configs/debugger.config.ts b/src/configs/debugger.config.ts new file mode 100644 index 0000000..31cfa47 --- /dev/null +++ b/src/configs/debugger.config.ts @@ -0,0 +1,22 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'debugger', + (): Record => ({ + http: { + writeIntoFile: process.env.DEBUGGER_HTTP_WRITE_INTO_FILE === 'true', + writeIntoConsole: + process.env.DEBUGGER_HTTP_WRITE_INTO_CONSOLE === 'true', + maxFiles: 5, + maxSize: '2M', + }, + system: { + writeIntoFile: + process.env.DEBUGGER_SYSTEM_WRITE_INTO_FILE === 'true', + writeIntoConsole: + process.env.DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE === 'true', + maxFiles: '7d', + maxSize: '2m', + }, + }) +); diff --git a/src/configs/doc.config.ts b/src/configs/doc.config.ts new file mode 100644 index 0000000..678822b --- /dev/null +++ b/src/configs/doc.config.ts @@ -0,0 +1,11 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'doc', + (): Record => ({ + name: `${process.env.APP_NAME} APIs Specification`, + description: 'Section for describe whole APIs', + version: '1.0', + prefix: '/docs', + }) +); diff --git a/src/configs/file.config.ts b/src/configs/file.config.ts new file mode 100644 index 0000000..28df479 --- /dev/null +++ b/src/configs/file.config.ts @@ -0,0 +1,26 @@ +import { registerAs } from '@nestjs/config'; +import bytes from 'bytes'; + +// if we use was api gateway, there has limitation of the payload size +// the payload size 10mb +export default registerAs( + 'file', + (): Record => ({ + image: { + maxFileSize: bytes('1mb'), // 1mb + maxFiles: 3, // 3 files + }, + excel: { + maxFileSize: bytes('5.5mb'), // 5.5mb + maxFiles: 1, // 1 files + }, + audio: { + maxFileSize: bytes('5.5mb'), // 5.5mb + maxFiles: 1, // 1 files + }, + video: { + maxFileSize: bytes('5.5mb'), // 5.5mb + maxFiles: 1, // 1 files + }, + }) +); diff --git a/src/configs/helper.config.ts b/src/configs/helper.config.ts new file mode 100644 index 0000000..d9c6192 --- /dev/null +++ b/src/configs/helper.config.ts @@ -0,0 +1,16 @@ +import { registerAs } from '@nestjs/config'; +import { seconds } from 'src/common/helper/constants/helper.function.constant'; + +export default registerAs( + 'helper', + (): Record => ({ + salt: { + length: 8, + }, + jwt: { + secretKey: '123456', + expirationTime: seconds('1h'), + notBeforeExpirationTime: seconds('0'), + }, + }) +); diff --git a/src/configs/index.ts b/src/configs/index.ts new file mode 100644 index 0000000..0bd099d --- /dev/null +++ b/src/configs/index.ts @@ -0,0 +1,23 @@ +import AppConfig from './app.config'; +import AuthConfig from './auth.config'; +import DatabaseConfig from './database.config'; +import HelperConfig from './helper.config'; +import AwsConfig from './aws.config'; +import UserConfig from './user.config'; +import FileConfig from './file.config'; +import RequestConfig from './request.config'; +import DocConfig from './doc.config'; +import DebuggerConfig from './debugger.config'; + +export default [ + AppConfig, + AuthConfig, + DatabaseConfig, + HelperConfig, + AwsConfig, + UserConfig, + RequestConfig, + FileConfig, + DocConfig, + DebuggerConfig, +]; diff --git a/src/configs/request.config.ts b/src/configs/request.config.ts new file mode 100644 index 0000000..2a62670 --- /dev/null +++ b/src/configs/request.config.ts @@ -0,0 +1,95 @@ +import { registerAs } from '@nestjs/config'; +import bytes from 'bytes'; +import ms from 'ms'; +import { seconds } from 'src/common/helper/constants/helper.function.constant'; +import { ENUM_REQUEST_METHOD } from 'src/common/request/constants/request.enum.constant'; + +export default registerAs( + 'request', + (): Record => ({ + body: { + json: { + maxFileSize: bytes('100kb'), // 100kb + }, + raw: { + maxFileSize: bytes('5.5mb'), // 5.5mb + }, + text: { + maxFileSize: bytes('100kb'), // 100kb + }, + urlencoded: { + maxFileSize: bytes('100kb'), // 100kb + }, + }, + timestamp: { + toleranceTimeInMs: ms('5m'), // 5 mins + }, + timeout: ms('30s'), // 30s based on ms module + userAgent: { + os: [ + 'Mobile', + 'Mac OS', + 'Windows', + 'UNIX', + 'Linux', + 'iOS', + 'Android', + ], + browser: [ + 'IE', + 'Safari', + 'Edge', + 'Opera', + 'Chrome', + 'Firefox', + 'Samsung Browser', + 'UCBrowser', + ], + }, + cors: { + allowMethod: [ + ENUM_REQUEST_METHOD.GET, + ENUM_REQUEST_METHOD.DELETE, + ENUM_REQUEST_METHOD.PUT, + ENUM_REQUEST_METHOD.PATCH, + ENUM_REQUEST_METHOD.POST, + ], + allowOrigin: '*', // allow all origin + // allowOrigin: [/example\.com(\:\d{1,4})?$/], // allow all subdomain, and all port + // allowOrigin: [/example\.com$/], // allow all subdomain without port + allowHeader: [ + 'Accept', + 'Accept-Language', + 'Content-Language', + 'Content-Type', + 'Origin', + 'Authorization', + 'Access-Control-Request-Method', + 'Access-Control-Request-Headers', + 'Access-Control-Allow-Headers', + 'Access-Control-Allow-Origin', + 'Access-Control-Allow-Methods', + 'Access-Control-Allow-Credentials', + 'Access-Control-Expose-Headers', + 'Access-Control-Max-Age', + 'Referer', + 'Host', + 'X-Requested-With', + 'x-custom-lang', + 'x-timestamp', + 'x-api-key', + 'x-timezone', + 'x-request-id', + 'x-version', + 'x-repo-version', + 'x-permission-token', + 'X-Response-Time', + 'user-agent', + ], + }, + throttle: { + ttl: seconds('500'), // 0.5 secs + limit: 10, // max request per reset time + }, + }) +); diff --git a/src/configs/user.config.ts b/src/configs/user.config.ts new file mode 100644 index 0000000..e10fe06 --- /dev/null +++ b/src/configs/user.config.ts @@ -0,0 +1,9 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'user', + (): Record => ({ + uploadPath: '/user', + mobileNumberCountryCodeAllowed: ['628', '658'], + }) +); diff --git a/src/health/controllers/health.controller.ts b/src/health/controllers/health.controller.ts new file mode 100644 index 0000000..c017096 --- /dev/null +++ b/src/health/controllers/health.controller.ts @@ -0,0 +1,117 @@ +import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { + DiskHealthIndicator, + HealthCheck, + HealthCheckService, + MemoryHealthIndicator, + MongooseHealthIndicator, +} from '@nestjs/terminus'; +import { Connection } from 'mongoose'; +import { DatabaseConnection } from 'src/common/database/decorators/database.decorator'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; +import { HealthCheckDoc } from 'src/health/docs/health.doc'; +import { HealthAwsS3Indicator } from 'src/health/indicators/health.aws-s3.indicator'; +import { HealthSerialization } from 'src/health/serializations/health.serialization'; + +@ApiTags('health') +@Controller({ + version: VERSION_NEUTRAL, + path: '/health', +}) +export class HealthController { + constructor( + @DatabaseConnection() private readonly databaseConnection: Connection, + private readonly health: HealthCheckService, + private readonly memoryHealthIndicator: MemoryHealthIndicator, + private readonly diskHealthIndicator: DiskHealthIndicator, + private readonly mongooseIndicator: MongooseHealthIndicator, + private readonly awsS3Indicator: HealthAwsS3Indicator + ) {} + + @HealthCheckDoc() + @Response('health.check', { serialization: HealthSerialization }) + @HealthCheck() + @Get('/aws') + async checkAws(): Promise { + const data = await this.health.check([ + () => this.awsS3Indicator.isHealthy('awsS3Bucket'), + ]); + + return { + data, + }; + } + + @HealthCheckDoc() + @Response('health.check', { serialization: HealthSerialization }) + @HealthCheck() + @Get('/database') + async checkDatabase(): Promise { + const data = await this.health.check([ + () => + this.mongooseIndicator.pingCheck('database', { + connection: this.databaseConnection, + }), + ]); + + return { + data, + }; + } + + @HealthCheckDoc() + @Response('health.check', { serialization: HealthSerialization }) + @HealthCheck() + @Get('/memory-heap') + async checkMemoryHeap(): Promise { + const data = await this.health.check([ + () => + this.memoryHealthIndicator.checkHeap( + 'memoryHeap', + 300 * 1024 * 1024 + ), + ]); + + return { + data, + }; + } + + @HealthCheckDoc() + @Response('health.check', { serialization: HealthSerialization }) + @HealthCheck() + @Get('/memory-rss') + async checkMemoryRss(): Promise { + const data = await this.health.check([ + () => + this.memoryHealthIndicator.checkRSS( + 'memoryRss', + 300 * 1024 * 1024 + ), + ]); + + return { + data, + }; + } + + @HealthCheckDoc() + @Response('health.check', { serialization: HealthSerialization }) + @HealthCheck() + @Get('/storage') + async checkStorage(): Promise { + const data = await this.health.check([ + () => + this.diskHealthIndicator.checkStorage('diskHealth', { + thresholdPercent: 0.75, + path: '/', + }), + ]); + + return { + data, + }; + } +} diff --git a/src/health/docs/health.doc.ts b/src/health/docs/health.doc.ts new file mode 100644 index 0000000..04b3373 --- /dev/null +++ b/src/health/docs/health.doc.ts @@ -0,0 +1,14 @@ +import { applyDecorators } from '@nestjs/common'; +import { Doc } from 'src/common/doc/decorators/doc.decorator'; +import { HealthSerialization } from 'src/health/serializations/health.serialization'; + +export function HealthCheckDoc(): MethodDecorator { + return applyDecorators( + Doc('health.check', { + auth: { + jwtAccessToken: false, + }, + response: { serialization: HealthSerialization }, + }) + ); +} diff --git a/src/health/health.module.ts b/src/health/health.module.ts new file mode 100644 index 0000000..770e9f8 --- /dev/null +++ b/src/health/health.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AwsModule } from 'src/common/aws/aws.module'; +import { HealthAwsS3Indicator } from 'src/health/indicators/health.aws-s3.indicator'; + +@Module({ + providers: [HealthAwsS3Indicator], + exports: [HealthAwsS3Indicator], + imports: [AwsModule], +}) +export class HealthModule {} diff --git a/src/health/indicators/health.aws-s3.indicator.ts b/src/health/indicators/health.aws-s3.indicator.ts new file mode 100644 index 0000000..62967ab --- /dev/null +++ b/src/health/indicators/health.aws-s3.indicator.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { + HealthCheckError, + HealthIndicator, + HealthIndicatorResult, +} from '@nestjs/terminus'; +import { AwsS3Service } from 'src/common/aws/services/aws.s3.service'; + +@Injectable() +export class HealthAwsS3Indicator extends HealthIndicator { + constructor(private readonly awsS3Service: AwsS3Service) { + super(); + } + + async isHealthy(key: string): Promise { + try { + await this.awsS3Service.checkConnection(); + return this.getStatus(key, true); + } catch (err: unknown) { + throw new HealthCheckError( + 'HealthAwsS3Indicator failed', + this.getStatus(key, false) + ); + } + } +} diff --git a/src/health/serializations/health.serialization.ts b/src/health/serializations/health.serialization.ts new file mode 100644 index 0000000..97d73fd --- /dev/null +++ b/src/health/serializations/health.serialization.ts @@ -0,0 +1,31 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class HealthSerialization { + @ApiProperty({ + example: 'ok', + }) + status: string; + + @ApiProperty({ + example: { + awsBucket: { + status: 'up', + }, + }, + }) + info: Record; + + @ApiProperty({ + example: {}, + }) + error: Record; + + @ApiProperty({ + example: { + awsBucket: { + status: 'up', + }, + }, + }) + details: Record; +} diff --git a/src/jobs/jobs.module.ts b/src/jobs/jobs.module.ts new file mode 100644 index 0000000..0e6d751 --- /dev/null +++ b/src/jobs/jobs.module.ts @@ -0,0 +1,27 @@ +import { DynamicModule, ForwardReference, Module, Type } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; +import { JobsRouterModule } from './router/jobs.router.module'; + +@Module({}) +export class JobsModule { + static forRoot(): DynamicModule { + const imports: ( + | DynamicModule + | Type + | Promise + | ForwardReference + )[] = []; + + if (process.env.JOB_ENABLE === 'true') { + imports.push(ScheduleModule.forRoot(), JobsRouterModule); + } + + return { + module: JobsModule, + providers: [], + exports: [], + controllers: [], + imports, + }; + } +} diff --git a/src/jobs/router/jobs.router.module.ts b/src/jobs/router/jobs.router.module.ts new file mode 100644 index 0000000..f745d22 --- /dev/null +++ b/src/jobs/router/jobs.router.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ApiKeyModule } from 'src/common/api-key/api-key.module'; +import { ApiKeyInactiveTask } from 'src/common/api-key/tasks/api-key.inactive.task'; + +@Module({ + providers: [ApiKeyInactiveTask], + exports: [], + imports: [ApiKeyModule], + controllers: [], +}) +export class JobsRouterModule {} diff --git a/src/languages/en/apiKey.json b/src/languages/en/apiKey.json new file mode 100644 index 0000000..3307592 --- /dev/null +++ b/src/languages/en/apiKey.json @@ -0,0 +1,13 @@ +{ + "error": { + "timestampInvalid": "Request timestamp not acceptable", + "keyNeeded": "Need API Key", + "prefixInvalid": "Prefix API Key invalid", + "schemaInvalid": "API Key Schema invalid", + "timestampNotMatchWithRequest": "Timestamp not match with request", + "notFound": "Auth API not found", + "inactive": "Auth API Inactive", + "invalid": "Invalid API Key", + "exist": "API Key Exist" + } +} \ No newline at end of file diff --git a/src/languages/en/app.json b/src/languages/en/app.json new file mode 100644 index 0000000..06fe878 --- /dev/null +++ b/src/languages/en/app.json @@ -0,0 +1,4 @@ +{ + "hello": "This is test endpoint service {serviceName}.", + "helloTimeout": "This is test endpoint service {serviceName} timeout." +} diff --git a/src/languages/en/auth.json b/src/languages/en/auth.json new file mode 100644 index 0000000..7761528 --- /dev/null +++ b/src/languages/en/auth.json @@ -0,0 +1,15 @@ +{ + "enum": { + "accessFor": "Enum access for succeed" + }, + "error": { + "passwordExpired": + "Password expired, go reset password", + "permissionForbidden": + "Permission not allowed", + "accessForForbidden": "Access for not allowed", + "accessTokenUnauthorized": "Access Token UnAuthorized", + "refreshTokenUnauthorized": "Refresh Token UnAuthorized" + } +} + diff --git a/src/languages/en/file.json b/src/languages/en/file.json new file mode 100644 index 0000000..ee13f47 --- /dev/null +++ b/src/languages/en/file.json @@ -0,0 +1,10 @@ +{ + "error": { + "notFound": "File not found", + "maxSize": "File size too big", + "maxFiles": "Files are to many", + "mimeInvalid": "File extension not valid", + "needExtractFirst": "Extract data needed", + "validationDto": "Import Data invalid" + } +} diff --git a/src/languages/en/health.json b/src/languages/en/health.json new file mode 100644 index 0000000..bbf65aa --- /dev/null +++ b/src/languages/en/health.json @@ -0,0 +1,3 @@ +{ + "check": "Healthy succeed" +} \ No newline at end of file diff --git a/src/languages/en/http.json b/src/languages/en/http.json new file mode 100644 index 0000000..b25f068 --- /dev/null +++ b/src/languages/en/http.json @@ -0,0 +1,67 @@ +{ + "success": { + "ok": "OK", + "created": "Created", + "accepted": "Accepted", + "noContent": "No Content" + }, + "redirection": { + "movePermanently": "Move Permanently", + "found": "Found", + "notModified": "Not Modified", + "temporaryRedirect": "Temporary Redirect", + "permanentRedirect": "Permanent Redirect" + }, + "clientError": { + "badRequest": "Bad Request", + "unauthorized": "Unauthorized", + "forbidden": "Forbidden", + "notFound": "Not Found", + "methodNotAllowed": "Not Allowed Method", + "notAcceptable": "Not Acceptable", + "payloadToLarge": "Payload To Large", + "uriToLarge": "Uri To Large", + "unsupportedMediaType": "Unsupported Media Type", + "unprocessableEntity": "Unprocessable Entity", + "tooManyRequest": "Too Many Request" + }, + "serverError": { + "internalServerError": "Internal Server Error", + "notImplemented": "Not Implemented", + "badGateway": "Bad Gateway", + "serviceUnavailable": "Service Unavailable", + "gatewayTimeout": "Gateway Timeout" + }, + + "200": "OK", + + "201": "Created", + "202": "Accepted", + "204": "No Content", + + "301": "Move Permanently", + "302": "Found", + "304": "Not Modified", + "307": "Temporary Redirect", + "308": "Permanent Redirect", + + "400": "Bad Request", + "401": "Unauthorized", + "403": "Forbidden", + "404": "Not Found", + "405": "Not Allowed Method", + "406": "Not Acceptable", + "413": "Payload To Large", + "414": "Uri To Large", + "415": "Unsupported Media Type", + "422": "Unprocessable Entity", + "429": "Too Many Request", + + "500": "Internal Server Error", + "501": "Not Implemented", + "502": "Bad Gateway", + "503": "Service Unavailable", + "504": "Gateway Timeout" + + +} diff --git a/src/languages/en/message.json b/src/languages/en/message.json new file mode 100644 index 0000000..8083371 --- /dev/null +++ b/src/languages/en/message.json @@ -0,0 +1,5 @@ +{ + "enum": { + "languages": "message enum languages" + } +} \ No newline at end of file diff --git a/src/languages/en/middleware.json b/src/languages/en/middleware.json new file mode 100644 index 0000000..30f52ea --- /dev/null +++ b/src/languages/en/middleware.json @@ -0,0 +1,7 @@ +{ + "error": { + "userAgentInvalid": "Request user agent not acceptable", + "userAgentOsInvalid": "Request user agent OS not acceptable", + "userAgentBrowserInvalid": "Request user agent Browser not acceptable" + } +} \ No newline at end of file diff --git a/src/languages/en/permission.json b/src/languages/en/permission.json new file mode 100644 index 0000000..fd9f191 --- /dev/null +++ b/src/languages/en/permission.json @@ -0,0 +1,11 @@ +{ + "list": "Permission list succeed", + "get": "Permission get succeed", + "update": "Permission update succeed", + "active": "Permission active succeed", + "inactive": "Permission inactive succeed", + "error": { + "notFound": "Permission not found", + "active": "Permission active status invalid" + } +} diff --git a/src/languages/en/request.json b/src/languages/en/request.json new file mode 100644 index 0000000..89602ec --- /dev/null +++ b/src/languages/en/request.json @@ -0,0 +1,30 @@ +{ + "validation": "Validation errors", + "min": "{property} has less elements than the minimum allowed.", + "max": "{property} has more elements than the maximum allowed.", + "maxLength": "{property} has more elements than the maximum allowed.", + "minLength": "{property} has less elements than the minimum allowed.", + "isString": "{property} should be a type of string.", + "isNotEmpty": "{property} cannot be empty.", + "isLowercase": "{property} should be lowercase.", + "isOptional": "{property} is optional.", + "isPositive": "{property} should be a positive number.", + "isEmail": "{property} should be a type of email.", + "isInt": "{property} should be a number.", + "isNumberString": "{property} should be a number.", + "isNumber": "{property} should be a number {value}.", + "isMongoId": "{property} should reference with mongo object id.", + "isBoolean": "{property} should be a boolean", + "IsStartWith": "{property} should start with {value}", + "isEnum": "{property} don't match with enum", + "isObject": "{property} should be a object", + "isArray": "{property} should be a array", + "arrayNotEmpty": "{property} array is not empty", + "minDate": "{property} has less date than the minimum allowed.", + "maxDate": "{property} has more elements than the maximum allowed.", + "isDate": "{property} should be a date", + "minDateGreaterThan": "{property} has less date than the {value}", + "isPasswordStrong": "{property} must have strong pattern", + "isPasswordMedium": "{property} must have medium pattern", + "isPasswordWeak": "{property} must have weak pattern" +} diff --git a/src/languages/en/role.json b/src/languages/en/role.json new file mode 100644 index 0000000..bc83383 --- /dev/null +++ b/src/languages/en/role.json @@ -0,0 +1,16 @@ +{ + "list": "List Role Success.", + "get": "Get Role Success.", + "create": "Create Succeed", + "delete": "Delete Succeed", + "update": "Update Succeed", + "inactive": "Inactive Succeed", + "active": "Active Succeed", + "error": { + "notFound": "Role not found", + "exist": "Role exist", + "active": "Role active invalid", + "used": "Role in used", + "inactive": "Role is inactive" + } +} diff --git a/src/languages/en/setting.json b/src/languages/en/setting.json new file mode 100644 index 0000000..59b2dc9 --- /dev/null +++ b/src/languages/en/setting.json @@ -0,0 +1,9 @@ +{ + "list": "List Setting Success.", + "get": "Get Setting Success.", + "getByName": "Get Setting by name Success.", + "update": "Update Succeed", + "error": { + "notFound": "Setting not found" + } +} diff --git a/src/languages/en/user.json b/src/languages/en/user.json new file mode 100644 index 0000000..24a856e --- /dev/null +++ b/src/languages/en/user.json @@ -0,0 +1,25 @@ +{ + "list": "List User Success.", + "get": "Get User Success.", + "create": "Create User Success.", + "delete": "Delete User Success.", + "update": "Update User Success.", + "profile": "Profile Success", + "upload": "Upload Success", + "inactive": "Inactive Succeed", + "active": "Active Succeed", + "login": "Login success.", + "refresh": "Refresh token success", + "signUp": "Sign up Success", + "error": { + "notFound": "User not found.", + "emailExist": "Email user used", + "mobileNumberExist": "Mobile Number user used", + "active": "User status active invalid", + "exist": "User exist", + "passwordNotMatch": "Password not match", + "newPasswordMustDifference": "Old password must difference", + "inactive": "User is inactive", + "passwordExpired": "User password expired" + } +} diff --git a/src/languages/id/app.json b/src/languages/id/app.json new file mode 100644 index 0000000..06fe878 --- /dev/null +++ b/src/languages/id/app.json @@ -0,0 +1,4 @@ +{ + "hello": "This is test endpoint service {serviceName}.", + "helloTimeout": "This is test endpoint service {serviceName} timeout." +} diff --git a/src/languages/id/request.json b/src/languages/id/request.json new file mode 100644 index 0000000..8f3bfd5 --- /dev/null +++ b/src/languages/id/request.json @@ -0,0 +1,30 @@ +{ + "default": "Validation errors", + "min": "{property} has less elements than the minimum allowed.", + "max": "{property} has more elements than the maximum allowed.", + "maxLength": "{property} has more elements than the maximum allowed.", + "minLength": "{property} has less elements than the minimum allowed.", + "isString": "{property} should be a type of string.", + "isNotEmpty": "{property} cannot be empty.", + "isLowercase": "{property} should be lowercase.", + "isOptional": "{property} is optional.", + "isPositive": "{property} should be a positive number.", + "isEmail": "{property} should be a type of email.", + "isInt": "{property} should be a number.", + "isNumberString": "{property} should be a number.", + "isNumber": "{property} should be a number {value}.", + "isMongoId": "{property} should reference with mongo object id.", + "isBoolean": "{property} should be a boolean", + "IsStartWith": "{property} should start with {value}", + "isEnum": "{property} don't match with enum", + "isObject": "{property} should be a object", + "isArray": "{property} should be a array", + "arrayNotEmpty": "{property} array is not empty", + "minDate": "{property} has less date than the minimum allowed.", + "maxDate": "{property} has more elements than the maximum allowed.", + "isDate": "{property} should be a date", + "minDateGreaterThan": "{property} has less date than the {value}", + "isPasswordStrong": "{property} must have strong pattern", + "isPasswordMedium": "{property} must have medium pattern", + "isPasswordWeak": "{property} must have weak pattern" +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..69048c2 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,74 @@ +import { NestApplication, NestFactory } from '@nestjs/core'; +import { Logger, VersioningType } from '@nestjs/common'; +import { AppModule } from 'src/app/app.module'; +import { ConfigService } from '@nestjs/config'; +import { useContainer } from 'class-validator'; +import swaggerInit from './swagger'; + +async function bootstrap() { + const app: NestApplication = await NestFactory.create(AppModule); + const configService = app.get(ConfigService); + const databaseUri: string = configService.get('database.host'); + const env: string = configService.get('app.env'); + const host: string = configService.get('app.http.host'); + const port: number = configService.get('app.http.port'); + const globalPrefix: string = configService.get('app.globalPrefix'); + const versioningPrefix: string = configService.get( + 'app.versioning.prefix' + ); + const version: string = configService.get('app.versioning.version'); + + // enable + const httpEnable: boolean = configService.get('app.http.enable'); + const versionEnable: string = configService.get( + 'app.versioning.enable' + ); + const jobEnable: boolean = configService.get('app.jobEnable'); + + const logger = new Logger(); + process.env.NODE_ENV = env; + + // Global + app.setGlobalPrefix(globalPrefix); + useContainer(app.select(AppModule), { fallbackOnErrors: true }); + + // Versioning + if (versionEnable) { + app.enableVersioning({ + type: VersioningType.URI, + defaultVersion: version, + prefix: versioningPrefix, + }); + } + + // Swagger + await swaggerInit(app); + + // Listen + await app.listen(port, host); + + logger.log(`==========================================================`); + + logger.log(`Environment Variable`, 'NestApplication'); + logger.log(JSON.parse(JSON.stringify(process.env)), 'NestApplication'); + + logger.log(`==========================================================`); + + logger.log(`Job is ${jobEnable}`, 'NestApplication'); + logger.log( + `Http is ${httpEnable}, ${ + httpEnable ? 'routes registered' : 'no routes registered' + }`, + 'NestApplication' + ); + logger.log(`Http versioning is ${versionEnable}`, 'NestApplication'); + + logger.log( + `Http Server running on ${await app.getUrl()}`, + 'NestApplication' + ); + logger.log(`Database uri ${databaseUri}`, 'NestApplication'); + + logger.log(`==========================================================`); +} +bootstrap(); diff --git a/src/migration/migration.module.ts b/src/migration/migration.module.ts new file mode 100644 index 0000000..0c4fe72 --- /dev/null +++ b/src/migration/migration.module.ts @@ -0,0 +1,34 @@ +import { Module } from '@nestjs/common'; +import { CommandModule } from 'nestjs-command'; +import { ApiKeyModule } from 'src/common/api-key/api-key.module'; +import { AuthModule } from 'src/common/auth/auth.module'; +import { CommonModule } from 'src/common/common.module'; +import { MigrationApiKeySeed } from 'src/migration/seeds/migration.api-key.seed'; +import { MigrationPermissionSeed } from 'src/migration/seeds/migration.permission.seed'; +import { MigrationRoleSeed } from 'src/migration/seeds/migration.role.seed'; +import { MigrationSettingSeed } from 'src/migration/seeds/migration.setting.seed'; +import { MigrationUserSeed } from 'src/migration/seeds/migration.user.seed'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { RoleModule } from 'src/modules/role/role.module'; +import { UserModule } from 'src/modules/user/user.module'; + +@Module({ + imports: [ + CommonModule, + CommandModule, + ApiKeyModule, + AuthModule, + PermissionModule, + UserModule, + RoleModule, + ], + providers: [ + MigrationApiKeySeed, + MigrationSettingSeed, + MigrationPermissionSeed, + MigrationRoleSeed, + MigrationUserSeed, + ], + exports: [], +}) +export class MigrationModule {} diff --git a/src/migration/seeds/migration.api-key.seed.ts b/src/migration/seeds/migration.api-key.seed.ts new file mode 100644 index 0000000..1a32231 --- /dev/null +++ b/src/migration/seeds/migration.api-key.seed.ts @@ -0,0 +1,49 @@ +import { Command } from 'nestjs-command'; +import { Injectable } from '@nestjs/common'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { UserService } from 'src/modules/user/services/user.service'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; + +@Injectable() +export class MigrationApiKeySeed { + constructor( + private readonly userService: UserService, + private readonly apiKeyService: ApiKeyService + ) {} + + @Command({ + command: 'seed:apikey', + describe: 'seeds apikeys', + }) + async seeds(): Promise { + try { + const user: UserDoc = await this.userService.findOneByUsername( + 'superadmin' + ); + await this.apiKeyService.createRaw(user._id, { + name: 'Api Key Migration', + description: 'From migration', + key: 'qwertyuiop12345zxcvbnmkjh', + secret: '5124512412412asdasdasdasdasdASDASDASD', + }); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } + + @Command({ + command: 'remove:apikey', + describe: 'remove apikeys', + }) + async remove(): Promise { + try { + await this.apiKeyService.deleteMany({}); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } +} diff --git a/src/migration/seeds/migration.permission.seed.ts b/src/migration/seeds/migration.permission.seed.ts new file mode 100644 index 0000000..47cae96 --- /dev/null +++ b/src/migration/seeds/migration.permission.seed.ts @@ -0,0 +1,55 @@ +import { Command } from 'nestjs-command'; +import { Injectable } from '@nestjs/common'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { PermissionCreateDto } from 'src/modules/permission/dtos/permission.create.dto'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { PermissionService } from 'src/modules/permission/services/permission.service'; + +@Injectable() +export class MigrationPermissionSeed { + constructor(private readonly permissionService: PermissionService) {} + + @Command({ + command: 'seed:permission', + describe: 'seed permissions', + }) + async seeds(): Promise { + try { + const permissions: string[] = Object.values(ENUM_AUTH_PERMISSIONS); + const group: string[] = Object.values(ENUM_PERMISSION_GROUP); + + const data: PermissionCreateDto[] = permissions.map((val) => { + const dto: PermissionCreateDto = new PermissionCreateDto(); + + dto.code = val; + dto.description = `${val.replace('_', ' ')} description`; + dto.group = group.find((l: string) => + val.startsWith(l) + ) as ENUM_PERMISSION_GROUP; + + return dto; + }) as PermissionEntity[]; + + await this.permissionService.createMany(data); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } + + @Command({ + command: 'remove:permission', + describe: 'remove permissions', + }) + async remove(): Promise { + try { + await this.permissionService.deleteMany({}); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } +} diff --git a/src/migration/seeds/migration.role.seed.ts b/src/migration/seeds/migration.role.seed.ts new file mode 100644 index 0000000..1514ed7 --- /dev/null +++ b/src/migration/seeds/migration.role.seed.ts @@ -0,0 +1,61 @@ +import { Command } from 'nestjs-command'; +import { Injectable } from '@nestjs/common'; +import { PermissionService } from 'src/modules/permission/services/permission.service'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { RoleCreateDto } from 'src/modules/role/dtos/role.create.dto'; + +@Injectable() +export class MigrationRoleSeed { + constructor( + private readonly permissionService: PermissionService, + private readonly roleService: RoleService + ) {} + + @Command({ + command: 'seed:role', + describe: 'seed roles', + }) + async seeds(): Promise { + const permissions: PermissionEntity[] = + await this.permissionService.findAll(); + const permissionsMap = permissions.map((val) => val._id); + + const dataAdmin: RoleCreateDto[] = [ + { + name: 'admin', + permissions: permissionsMap, + accessFor: ENUM_AUTH_ACCESS_FOR.ADMIN, + }, + { + name: 'user', + permissions: [], + accessFor: ENUM_AUTH_ACCESS_FOR.USER, + }, + ]; + + try { + await this.roleService.createMany(dataAdmin); + await this.roleService.createSuperAdmin(); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } + + @Command({ + command: 'remove:role', + describe: 'remove roles', + }) + async remove(): Promise { + try { + await this.roleService.deleteMany({}); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } +} diff --git a/src/migration/seeds/migration.setting.seed.ts b/src/migration/seeds/migration.setting.seed.ts new file mode 100644 index 0000000..704edd2 --- /dev/null +++ b/src/migration/seeds/migration.setting.seed.ts @@ -0,0 +1,42 @@ +import { Command } from 'nestjs-command'; +import { Injectable } from '@nestjs/common'; +import { SettingService } from 'src/common/setting/services/setting.service'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; + +@Injectable() +export class MigrationSettingSeed { + constructor(private readonly settingService: SettingService) {} + + @Command({ + command: 'seed:setting', + describe: 'seeds settings', + }) + async seeds(): Promise { + try { + await this.settingService.create({ + name: 'maintenance', + description: 'Maintenance Mode', + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + value: 'false', + }); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } + + @Command({ + command: 'remove:setting', + describe: 'remove settings', + }) + async remove(): Promise { + try { + await this.settingService.deleteMany({}); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } +} diff --git a/src/migration/seeds/migration.user.seed.ts b/src/migration/seeds/migration.user.seed.ts new file mode 100644 index 0000000..5acdfac --- /dev/null +++ b/src/migration/seeds/migration.user.seed.ts @@ -0,0 +1,97 @@ +import { Command } from 'nestjs-command'; +import { Injectable } from '@nestjs/common'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { UserService } from 'src/modules/user/services/user.service'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; + +@Injectable() +export class MigrationUserSeed { + constructor( + private readonly authService: AuthService, + private readonly userService: UserService, + private readonly roleService: RoleService + ) {} + + @Command({ + command: 'seed:user', + describe: 'seed users', + }) + async seeds(): Promise { + const password = 'aaAA@@123444'; + const superadminRole: RoleDoc = await this.roleService.findOne({ + name: 'superadmin', + }); + const adminRole: RoleDoc = await this.roleService.findOne({ + name: 'admin', + }); + const userRole: RoleDoc = await this.roleService.findOne({ + name: 'user', + }); + const passwordHash = await this.authService.createPassword( + 'aaAA@@123444' + ); + + const user1: Promise = this.userService.create( + { + username: 'superadmin', + firstName: 'superadmin', + lastName: 'test', + email: 'superadmin@mail.com', + password, + mobileNumber: '08111111222', + role: superadminRole._id, + }, + passwordHash + ); + + const user2: Promise = this.userService.create( + { + username: 'admin', + firstName: 'admin', + lastName: 'test', + email: 'admin@mail.com', + password, + mobileNumber: '08111111111', + role: adminRole._id, + }, + passwordHash + ); + + const user3: Promise = this.userService.create( + { + username: 'user', + firstName: 'user', + lastName: 'test', + email: 'user@mail.com', + password, + mobileNumber: '08111111333', + role: userRole._id, + }, + passwordHash + ); + + try { + await Promise.all([user1, user2, user3]); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } + + @Command({ + command: 'remove:user', + describe: 'remove users', + }) + async remove(): Promise { + try { + await this.userService.deleteMany({}); + } catch (err: any) { + throw new Error(err.message); + } + + return; + } +} diff --git a/src/modules/permission/constants/permission.constant.ts b/src/modules/permission/constants/permission.constant.ts new file mode 100644 index 0000000..1d6e8f5 --- /dev/null +++ b/src/modules/permission/constants/permission.constant.ts @@ -0,0 +1 @@ +export const PERMISSION_ACTIVE_META_KEY = 'PermissionActiveMetaKey'; diff --git a/src/modules/permission/constants/permission.doc.constant.ts b/src/modules/permission/constants/permission.doc.constant.ts new file mode 100644 index 0000000..2c65d93 --- /dev/null +++ b/src/modules/permission/constants/permission.doc.constant.ts @@ -0,0 +1,34 @@ +import { faker } from '@faker-js/faker'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; + +export const PermissionDocQueryIsActive = [ + { + name: 'isActive', + allowEmptyValue: false, + required: true, + type: 'string', + example: 'true,false', + description: "boolean value with ',' delimiter", + }, +]; + +export const PermissionDocQueryGroup = [ + { + name: 'group', + allowEmptyValue: false, + required: true, + type: 'string', + example: `${ENUM_PERMISSION_GROUP.PERMISSION},${ENUM_PERMISSION_GROUP.ROLE}`, + description: "group permissions value with ',' delimiter", + }, +]; + +export const PermissionDocParamsGet = [ + { + name: 'permissions', + allowEmptyValue: false, + required: true, + type: 'string', + example: faker.datatype.uuid(), + }, +]; diff --git a/src/modules/permission/constants/permission.enum.constant.ts b/src/modules/permission/constants/permission.enum.constant.ts new file mode 100644 index 0000000..f9b8c4d --- /dev/null +++ b/src/modules/permission/constants/permission.enum.constant.ts @@ -0,0 +1,7 @@ +export enum ENUM_PERMISSION_GROUP { + USER = 'USER', + ROLE = 'ROLE', + PERMISSION = 'PERMISSION', + API_KEY = 'API_KEY', + SETTING = 'SETTING', +} diff --git a/src/modules/permission/constants/permission.list.constant.ts b/src/modules/permission/constants/permission.list.constant.ts new file mode 100644 index 0000000..0c4b46f --- /dev/null +++ b/src/modules/permission/constants/permission.list.constant.ts @@ -0,0 +1,15 @@ +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; + +export const PERMISSION_DEFAULT_ORDER_BY = 'createdAt'; +export const PERMISSION_DEFAULT_ORDER_DIRECTION = + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC; +export const PERMISSION_DEFAULT_PER_PAGE = 20; +export const PERMISSION_DEFAULT_AVAILABLE_ORDER_BY = [ + 'code', + 'name', + 'createdAt', +]; +export const PERMISSION_DEFAULT_AVAILABLE_SEARCH = ['code', 'name']; +export const PERMISSION_DEFAULT_IS_ACTIVE = [true, false]; +export const PERMISSION_DEFAULT_GROUP = Object.values(ENUM_PERMISSION_GROUP); diff --git a/src/modules/permission/constants/permission.status-code.constant.ts b/src/modules/permission/constants/permission.status-code.constant.ts new file mode 100644 index 0000000..63180a8 --- /dev/null +++ b/src/modules/permission/constants/permission.status-code.constant.ts @@ -0,0 +1,6 @@ +export enum ENUM_PERMISSION_STATUS_CODE_ERROR { + PERMISSION_NOT_FOUND_ERROR = 5200, + PERMISSION_GUARD_INVALID_ERROR = 5201, + PERMISSION_IS_ACTIVE_ERROR = 5202, + PERMISSION_INACTIVE_ERROR = 5203, +} diff --git a/src/modules/permission/controllers/permission.admin.controller.ts b/src/modules/permission/controllers/permission.admin.controller.ts new file mode 100644 index 0000000..dc9db86 --- /dev/null +++ b/src/modules/permission/controllers/permission.admin.controller.ts @@ -0,0 +1,253 @@ +import { + Body, + Controller, + Get, + InternalServerErrorException, + Patch, + Put, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { AuthJwtAdminAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator'; +import { AuthPermissionProtected } from 'src/common/auth/decorators/auth.permission.decorator'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { + PaginationQuery, + PaginationQueryFilterInBoolean, + PaginationQueryFilterInEnum, +} from 'src/common/pagination/decorators/pagination.decorator'; +import { PaginationListDto } from 'src/common/pagination/dtos/pagination.list.dto'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { RequestParamGuard } from 'src/common/request/decorators/request.decorator'; +import { + Response, + ResponsePaging, +} from 'src/common/response/decorators/response.decorator'; +import { + IResponse, + IResponsePaging, +} from 'src/common/response/interfaces/response.interface'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; +import { + PERMISSION_DEFAULT_AVAILABLE_ORDER_BY, + PERMISSION_DEFAULT_AVAILABLE_SEARCH, + PERMISSION_DEFAULT_GROUP, + PERMISSION_DEFAULT_IS_ACTIVE, + PERMISSION_DEFAULT_ORDER_BY, + PERMISSION_DEFAULT_ORDER_DIRECTION, + PERMISSION_DEFAULT_PER_PAGE, +} from 'src/modules/permission/constants/permission.list.constant'; +import { + PermissionGetGuard, + PermissionUpdateActiveGuard, + PermissionUpdateGuard, + PermissionUpdateInactiveGuard, +} from 'src/modules/permission/decorators/permission.admin.decorator'; +import { GetPermission } from 'src/modules/permission/decorators/permission.decorator'; +import { + PermissionActiveDoc, + PermissionGetDoc, + PermissionGroupDoc, + PermissionInactiveDoc, + PermissionListDoc, + PermissionUpdateDoc, +} from 'src/modules/permission/docs/permission.admin.doc'; +import { PermissionUpdateDescriptionDto } from 'src/modules/permission/dtos/permission.update-description.dto'; +import { PermissionRequestDto } from 'src/modules/permission/dtos/permissions.request.dto'; +import { IPermissionGroup } from 'src/modules/permission/interfaces/permission.interface'; +import { + PermissionDoc, + PermissionEntity, +} from 'src/modules/permission/repository/entities/permission.entity'; +import { PermissionGetSerialization } from 'src/modules/permission/serializations/permission.get.serialization'; +import { PermissionGroupsSerialization } from 'src/modules/permission/serializations/permission.group.serialization'; +import { PermissionListSerialization } from 'src/modules/permission/serializations/permission.list.serialization'; +import { PermissionService } from 'src/modules/permission/services/permission.service'; + +@ApiTags('modules.admin.permission') +@Controller({ + version: '1', + path: '/permission', +}) +export class PermissionAdminController { + constructor( + private readonly paginationService: PaginationService, + private readonly permissionService: PermissionService + ) {} + + @PermissionListDoc() + @ResponsePaging('permission.list', { + serialization: PermissionListSerialization, + }) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.PERMISSION_READ) + @AuthJwtAdminAccessProtected() + @Get('/list') + async list( + @PaginationQuery( + PERMISSION_DEFAULT_PER_PAGE, + PERMISSION_DEFAULT_ORDER_BY, + PERMISSION_DEFAULT_ORDER_DIRECTION, + PERMISSION_DEFAULT_AVAILABLE_SEARCH, + PERMISSION_DEFAULT_AVAILABLE_ORDER_BY + ) + { _search, _limit, _offset, _order }: PaginationListDto, + @PaginationQueryFilterInBoolean( + 'isActive', + PERMISSION_DEFAULT_IS_ACTIVE + ) + isActive: Record, + @PaginationQueryFilterInEnum( + 'group', + PERMISSION_DEFAULT_GROUP, + ENUM_PERMISSION_GROUP + ) + group: Record + ): Promise { + const find: Record = { + ...isActive, + ..._search, + ...group, + }; + + const permissions: PermissionEntity[] = + await this.permissionService.findAll(find, { + paging: { + limit: _limit, + offset: _offset, + }, + order: _order, + }); + + const total: number = await this.permissionService.getTotal(find); + const totalPage: number = this.paginationService.totalPage( + total, + _limit + ); + + return { + _pagination: { total, totalPage }, + data: permissions, + }; + } + + @PermissionGroupDoc() + @Response('permission.group', { + serialization: PermissionGroupsSerialization, + }) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.PERMISSION_READ) + @AuthJwtAdminAccessProtected() + @Get('/group') + async group( + @PaginationQueryFilterInEnum( + 'groups', + PERMISSION_DEFAULT_GROUP, + ENUM_PERMISSION_GROUP + ) + groups: Record + ): Promise { + const permissions: PermissionDoc[] = + await this.permissionService.findAllByGroup(groups); + + const permissionGroups: IPermissionGroup[] = + await this.permissionService.groupingByGroups(permissions); + + return { data: { groups: permissionGroups } }; + } + + @PermissionGetDoc() + @Response('permission.get', { + serialization: PermissionGetSerialization, + }) + @PermissionGetGuard() + @RequestParamGuard(PermissionRequestDto) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.PERMISSION_READ) + @AuthJwtAdminAccessProtected() + @Get('/get/:permission') + async get( + @GetPermission(true) permission: PermissionEntity + ): Promise { + return { data: permission }; + } + + @PermissionUpdateDoc() + @Response('permission.update', { + serialization: ResponseIdSerialization, + }) + @PermissionUpdateGuard() + @RequestParamGuard(PermissionRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.PERMISSION_READ, + ENUM_AUTH_PERMISSIONS.PERMISSION_UPDATE + ) + @AuthJwtAdminAccessProtected() + @Put('/update/:permission') + async update( + @GetPermission() permission: PermissionDoc, + @Body() body: PermissionUpdateDescriptionDto + ): Promise { + try { + await this.permissionService.updateDescription(permission, body); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return { + data: { _id: permission._id }, + }; + } + + @PermissionInactiveDoc() + @Response('permission.inactive') + @PermissionUpdateInactiveGuard() + @RequestParamGuard(PermissionRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.PERMISSION_READ, + ENUM_AUTH_PERMISSIONS.PERMISSION_UPDATE, + ENUM_AUTH_PERMISSIONS.PERMISSION_INACTIVE + ) + @AuthJwtAdminAccessProtected() + @Patch('/update/:permission/inactive') + async inactive(@GetPermission() permission: PermissionDoc): Promise { + try { + await this.permissionService.inactive(permission); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @PermissionActiveDoc() + @Response('permission.active', {}) + @PermissionUpdateActiveGuard() + @RequestParamGuard(PermissionRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.PERMISSION_READ, + ENUM_AUTH_PERMISSIONS.PERMISSION_UPDATE, + ENUM_AUTH_PERMISSIONS.PERMISSION_ACTIVE + ) + @AuthJwtAdminAccessProtected() + @Patch('/update/:permission/active') + async active(@GetPermission() permission: PermissionDoc): Promise { + try { + await this.permissionService.active(permission); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } +} diff --git a/src/modules/permission/decorators/permission.admin.decorator.ts b/src/modules/permission/decorators/permission.admin.decorator.ts new file mode 100644 index 0000000..e7a20e4 --- /dev/null +++ b/src/modules/permission/decorators/permission.admin.decorator.ts @@ -0,0 +1,44 @@ +import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'; +import { PERMISSION_ACTIVE_META_KEY } from 'src/modules/permission/constants/permission.constant'; +import { PermissionActiveGuard } from 'src/modules/permission/guards/permission.active.guard'; +import { PermissionNotFoundGuard } from 'src/modules/permission/guards/permission.not-found.guard'; +import { PermissionPutToRequestGuard } from 'src/modules/permission/guards/permission.put-to-request.guard'; + +export function PermissionGetGuard(): MethodDecorator { + return applyDecorators( + UseGuards(PermissionPutToRequestGuard, PermissionNotFoundGuard) + ); +} + +export function PermissionUpdateGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + PermissionPutToRequestGuard, + PermissionNotFoundGuard, + PermissionActiveGuard + ), + SetMetadata(PERMISSION_ACTIVE_META_KEY, [true]) + ); +} + +export function PermissionUpdateActiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + PermissionPutToRequestGuard, + PermissionNotFoundGuard, + PermissionActiveGuard + ), + SetMetadata(PERMISSION_ACTIVE_META_KEY, [false]) + ); +} + +export function PermissionUpdateInactiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + PermissionPutToRequestGuard, + PermissionNotFoundGuard, + PermissionActiveGuard + ), + SetMetadata(PERMISSION_ACTIVE_META_KEY, [true]) + ); +} diff --git a/src/modules/permission/decorators/permission.decorator.ts b/src/modules/permission/decorators/permission.decorator.ts new file mode 100644 index 0000000..e494789 --- /dev/null +++ b/src/modules/permission/decorators/permission.decorator.ts @@ -0,0 +1,15 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { + PermissionDoc, + PermissionEntity, +} from 'src/modules/permission/repository/entities/permission.entity'; + +export const GetPermission = createParamDecorator( + ( + returnPlain: boolean, + ctx: ExecutionContext + ): PermissionDoc | PermissionEntity => { + const { __permission } = ctx.switchToHttp().getRequest(); + return returnPlain ? __permission.toObject() : __permission; + } +); diff --git a/src/modules/permission/docs/permission.admin.doc.ts b/src/modules/permission/docs/permission.admin.doc.ts new file mode 100644 index 0000000..8c1029a --- /dev/null +++ b/src/modules/permission/docs/permission.admin.doc.ts @@ -0,0 +1,106 @@ +import { applyDecorators } from '@nestjs/common'; +import { Doc, DocPaging } from 'src/common/doc/decorators/doc.decorator'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { + PermissionDocParamsGet, + PermissionDocQueryGroup, + PermissionDocQueryIsActive, +} from 'src/modules/permission/constants/permission.doc.constant'; +import { PermissionGetSerialization } from 'src/modules/permission/serializations/permission.get.serialization'; +import { PermissionGroupsSerialization } from 'src/modules/permission/serializations/permission.group.serialization'; +import { PermissionListSerialization } from 'src/modules/permission/serializations/permission.list.serialization'; + +export function PermissionListDoc(): MethodDecorator { + return applyDecorators( + DocPaging('permission.list', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + queries: [ + ...PermissionDocQueryIsActive, + ...PermissionDocQueryGroup, + ], + }, + response: { + serialization: PermissionListSerialization, + }, + }) + ); +} + +export function PermissionGetDoc(): MethodDecorator { + return applyDecorators( + Doc('permission.get', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: PermissionDocParamsGet, + }, + response: { serialization: PermissionGetSerialization }, + }) + ); +} + +export function PermissionUpdateDoc(): MethodDecorator { + return applyDecorators( + Doc('permission.update', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: PermissionDocParamsGet, + }, + response: { serialization: ResponseIdSerialization }, + }) + ); +} + +export function PermissionActiveDoc(): MethodDecorator { + return applyDecorators( + Doc('permission.active', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: PermissionDocParamsGet, + }, + }) + ); +} + +export function PermissionInactiveDoc(): MethodDecorator { + return applyDecorators( + Doc('permission.inactive', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: PermissionDocParamsGet, + }, + }) + ); +} + +export function PermissionGroupDoc(): MethodDecorator { + return applyDecorators( + Doc('permission.group', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + queries: PermissionDocQueryGroup, + }, + response: { + serialization: PermissionGroupsSerialization, + }, + }) + ); +} diff --git a/src/modules/permission/dtos/permission.active.dto.ts b/src/modules/permission/dtos/permission.active.dto.ts new file mode 100644 index 0000000..24918ad --- /dev/null +++ b/src/modules/permission/dtos/permission.active.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsBoolean } from 'class-validator'; + +export class PermissionActiveDto { + @ApiProperty({ + name: 'isActive', + description: 'is active permission', + required: true, + nullable: false, + }) + @IsBoolean() + @IsNotEmpty() + isActive: boolean; +} diff --git a/src/modules/permission/dtos/permission.create.dto.ts b/src/modules/permission/dtos/permission.create.dto.ts new file mode 100644 index 0000000..a96789b --- /dev/null +++ b/src/modules/permission/dtos/permission.create.dto.ts @@ -0,0 +1,33 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsEnum } from 'class-validator'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; + +export class PermissionCreateDto { + @ApiProperty({ + description: 'Permission group', + example: 'PERMISSION', + required: true, + }) + @IsEnum(ENUM_PERMISSION_GROUP) + @IsNotEmpty() + group: ENUM_PERMISSION_GROUP; + + @ApiProperty({ + description: 'Unique code of permission', + example: faker.random.alpha(5), + required: true, + }) + @IsString() + @IsNotEmpty() + code: string; + + @ApiProperty({ + description: 'Description of permission', + example: 'blabla description', + required: true, + }) + @IsString() + @IsNotEmpty() + description: string; +} diff --git a/src/modules/permission/dtos/permission.update-description.dto.ts b/src/modules/permission/dtos/permission.update-description.dto.ts new file mode 100644 index 0000000..1f43ff6 --- /dev/null +++ b/src/modules/permission/dtos/permission.update-description.dto.ts @@ -0,0 +1,7 @@ +import { PickType } from '@nestjs/swagger'; +import { PermissionCreateDto } from './permission.create.dto'; + +export class PermissionUpdateDescriptionDto extends PickType( + PermissionCreateDto, + ['description'] as const +) {} diff --git a/src/modules/permission/dtos/permission.update-group.dto.ts b/src/modules/permission/dtos/permission.update-group.dto.ts new file mode 100644 index 0000000..c79ca28 --- /dev/null +++ b/src/modules/permission/dtos/permission.update-group.dto.ts @@ -0,0 +1,6 @@ +import { PickType } from '@nestjs/swagger'; +import { PermissionCreateDto } from 'src/modules/permission/dtos/permission.create.dto'; + +export class PermissionUpdateGroupDto extends PickType(PermissionCreateDto, [ + 'group', +] as const) {} diff --git a/src/modules/permission/dtos/permissions.request.dto.ts b/src/modules/permission/dtos/permissions.request.dto.ts new file mode 100644 index 0000000..c6a5da8 --- /dev/null +++ b/src/modules/permission/dtos/permissions.request.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class PermissionRequestDto { + @ApiProperty({ + name: 'permission', + description: 'permission id', + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsUUID('4') + @Type(() => String) + permission: string; +} diff --git a/src/modules/permission/guards/permission.active.guard.ts b/src/modules/permission/guards/permission.active.guard.ts new file mode 100644 index 0000000..1aae130 --- /dev/null +++ b/src/modules/permission/guards/permission.active.guard.ts @@ -0,0 +1,36 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { PERMISSION_ACTIVE_META_KEY } from 'src/modules/permission/constants/permission.constant'; +import { ENUM_PERMISSION_STATUS_CODE_ERROR } from 'src/modules/permission/constants/permission.status-code.constant'; + +@Injectable() +export class PermissionActiveGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const required: boolean[] = this.reflector.getAllAndOverride( + PERMISSION_ACTIVE_META_KEY, + [context.getHandler(), context.getClass()] + ); + + if (!required) { + return true; + } + + const { __permission } = context.switchToHttp().getRequest(); + + if (!required.includes(__permission.isActive)) { + throw new BadRequestException({ + statusCode: + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_IS_ACTIVE_ERROR, + message: 'permission.error.isActiveInvalid', + }); + } + return true; + } +} diff --git a/src/modules/permission/guards/permission.not-found.guard.ts b/src/modules/permission/guards/permission.not-found.guard.ts new file mode 100644 index 0000000..3c90903 --- /dev/null +++ b/src/modules/permission/guards/permission.not-found.guard.ts @@ -0,0 +1,23 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + NotFoundException, +} from '@nestjs/common'; +import { ENUM_PERMISSION_STATUS_CODE_ERROR } from 'src/modules/permission/constants/permission.status-code.constant'; + +@Injectable() +export class PermissionNotFoundGuard implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + const { __permission } = context.switchToHttp().getRequest(); + + if (!__permission) { + throw new NotFoundException({ + statusCode: + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR, + message: 'permission.error.notFound', + }); + } + return true; + } +} diff --git a/src/modules/permission/guards/permission.put-to-request.guard.ts b/src/modules/permission/guards/permission.put-to-request.guard.ts new file mode 100644 index 0000000..e4dd08a --- /dev/null +++ b/src/modules/permission/guards/permission.put-to-request.guard.ts @@ -0,0 +1,21 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { PermissionDoc } from 'src/modules/permission/repository/entities/permission.entity'; +import { PermissionService } from 'src/modules/permission/services/permission.service'; + +@Injectable() +export class PermissionPutToRequestGuard implements CanActivate { + constructor(private readonly permissionService: PermissionService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { params } = request; + const { permission } = params; + + const check: PermissionDoc = await this.permissionService.findOneById( + permission + ); + request.__permission = check; + + return true; + } +} diff --git a/src/modules/permission/interfaces/permission.interface.ts b/src/modules/permission/interfaces/permission.interface.ts new file mode 100644 index 0000000..851d0c7 --- /dev/null +++ b/src/modules/permission/interfaces/permission.interface.ts @@ -0,0 +1,7 @@ +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; + +export interface IPermissionGroup { + group: ENUM_PERMISSION_GROUP; + permissions: PermissionEntity[]; +} diff --git a/src/modules/permission/interfaces/permission.service.interface.ts b/src/modules/permission/interfaces/permission.service.interface.ts new file mode 100644 index 0000000..92aa096 --- /dev/null +++ b/src/modules/permission/interfaces/permission.service.interface.ts @@ -0,0 +1,85 @@ +import { + IDatabaseCreateOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseCreateManyOptions, + IDatabaseManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { PermissionCreateDto } from 'src/modules/permission/dtos/permission.create.dto'; +import { PermissionUpdateGroupDto } from 'src/modules/permission/dtos/permission.update-group.dto'; +import { PermissionUpdateDescriptionDto } from 'src/modules/permission/dtos/permission.update-description.dto'; +import { IPermissionGroup } from 'src/modules/permission/interfaces/permission.interface'; +import { + PermissionDoc, + PermissionEntity, +} from 'src/modules/permission/repository/entities/permission.entity'; +import { ENUM_PERMISSION_GROUP } from "../constants/permission.enum.constant"; + +export interface IPermissionService { + findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + + findAllByIds( + ids: string[], + options?: IDatabaseFindAllOptions + ): Promise; + + findAllByGroup( + filterGroups?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + + findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise; + + findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise; + + getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise; + + delete(repository: PermissionDoc): Promise; + + create( + { group, code, description }: PermissionCreateDto, + options?: IDatabaseCreateOptions + ): Promise; + + updateDescription( + repository: PermissionDoc, + { description }: PermissionUpdateDescriptionDto + ): Promise; + + updateGroup( + repository: PermissionDoc, + data: PermissionUpdateGroupDto + ): Promise; + + active(repository: PermissionDoc): Promise; + + inactive(repository: PermissionDoc): Promise; + + groupingByGroups( + permissions: PermissionDoc[], + scope?: ENUM_PERMISSION_GROUP[] + ): Promise; + + createMany( + data: PermissionCreateDto[], + options?: IDatabaseCreateManyOptions + ): Promise; + + deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise; +} diff --git a/src/modules/permission/permission.module.ts b/src/modules/permission/permission.module.ts new file mode 100644 index 0000000..d440724 --- /dev/null +++ b/src/modules/permission/permission.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { PermissionRepositoryModule } from 'src/modules/permission/repository/permission.repository.module'; +import { PermissionService } from './services/permission.service'; + +@Module({ + controllers: [], + providers: [PermissionService], + exports: [PermissionService], + imports: [PermissionRepositoryModule], +}) +export class PermissionModule {} diff --git a/src/modules/permission/repository/entities/permission.entity.ts b/src/modules/permission/repository/entities/permission.entity.ts new file mode 100644 index 0000000..643eeb2 --- /dev/null +++ b/src/modules/permission/repository/entities/permission.entity.ts @@ -0,0 +1,58 @@ +import { Prop, SchemaFactory } from '@nestjs/mongoose'; +import { CallbackWithoutResultAndOptionalError, Document } from 'mongoose'; +import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntity } from 'src/common/database/decorators/database.decorator'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; + +export const PermissionDatabaseName = 'permissions'; + +@DatabaseEntity({ collection: PermissionDatabaseName }) +export class PermissionEntity extends DatabaseMongoUUIDEntityAbstract { + @Prop({ + required: true, + index: true, + unique: true, + uppercase: true, + trim: true, + maxlength: 25, + type: String, + }) + code: string; + + @Prop({ + required: true, + index: true, + trim: true, + enum: ENUM_PERMISSION_GROUP, + type: String, + }) + group: ENUM_PERMISSION_GROUP; + + @Prop({ + required: true, + type: String, + maxlength: 255, + }) + description: string; + + @Prop({ + required: true, + default: true, + index: true, + type: Boolean, + }) + isActive: boolean; +} + +export const PermissionSchema = SchemaFactory.createForClass(PermissionEntity); + +export type PermissionDoc = PermissionEntity & Document; + +PermissionSchema.pre( + 'save', + function (next: CallbackWithoutResultAndOptionalError) { + this.code = this.code.toUpperCase(); + + next(); + } +); diff --git a/src/modules/permission/repository/permission.repository.module.ts b/src/modules/permission/repository/permission.repository.module.ts new file mode 100644 index 0000000..f475a31 --- /dev/null +++ b/src/modules/permission/repository/permission.repository.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { + PermissionEntity, + PermissionSchema, +} from 'src/modules/permission/repository/entities/permission.entity'; +import { PermissionRepository } from 'src/modules/permission/repository/repositories/permission.repository'; + +@Module({ + providers: [PermissionRepository], + exports: [PermissionRepository], + controllers: [], + imports: [ + MongooseModule.forFeature( + [ + { + name: PermissionEntity.name, + schema: PermissionSchema, + }, + ], + DATABASE_CONNECTION_NAME + ), + ], +}) +export class PermissionRepositoryModule {} diff --git a/src/modules/permission/repository/repositories/permission.repository.ts b/src/modules/permission/repository/repositories/permission.repository.ts new file mode 100644 index 0000000..e59c92d --- /dev/null +++ b/src/modules/permission/repository/repositories/permission.repository.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { Model } from 'mongoose'; +import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; +import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; +import { + PermissionDoc, + PermissionEntity, +} from 'src/modules/permission/repository/entities/permission.entity'; + +@Injectable() +export class PermissionRepository extends DatabaseMongoUUIDRepositoryAbstract< + PermissionEntity, + PermissionDoc +> { + constructor( + @DatabaseModel(PermissionEntity.name) + private readonly permissionModel: Model + ) { + super(permissionModel); + } +} diff --git a/src/modules/permission/serializations/permission.get.serialization.ts b/src/modules/permission/serializations/permission.get.serialization.ts new file mode 100644 index 0000000..21e7b5c --- /dev/null +++ b/src/modules/permission/serializations/permission.get.serialization.ts @@ -0,0 +1,51 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; + +export class PermissionGetSerialization extends ResponseIdSerialization { + @ApiProperty({ + description: 'Active flag of permission', + example: true, + required: true, + }) + readonly isActive: boolean; + + @ApiProperty({ + description: 'Unique code of permission', + example: faker.random.alpha(5), + required: true, + }) + readonly code: string; + + @ApiProperty({ + enum: ENUM_PERMISSION_GROUP, + type: 'string', + }) + readonly group: ENUM_PERMISSION_GROUP; + + @ApiProperty({ + description: 'Description of permission', + example: 'blabla description', + required: false, + }) + readonly description?: string; + + @ApiProperty({ + description: 'Date created at', + example: faker.date.recent(), + required: true, + }) + readonly createdAt: Date; + + @ApiProperty({ + description: 'Date updated at', + example: faker.date.recent(), + required: false, + }) + readonly updatedAt: Date; + + @Exclude() + readonly deletedAt?: Date; +} diff --git a/src/modules/permission/serializations/permission.group.serialization.ts b/src/modules/permission/serializations/permission.group.serialization.ts new file mode 100644 index 0000000..3b039b8 --- /dev/null +++ b/src/modules/permission/serializations/permission.group.serialization.ts @@ -0,0 +1,35 @@ +import { ApiProperty, OmitType, PickType } from '@nestjs/swagger'; +import { Exclude, Type } from 'class-transformer'; +import { PermissionGetSerialization } from 'src/modules/permission/serializations/permission.get.serialization'; + +class PermissionGroupPermissionSerialization extends OmitType( + PermissionGetSerialization, + ['group', 'createdAt', 'updatedAt'] as const +) { + @Exclude() + group: Date; + + @Exclude() + createdAt: Date; + + @Exclude() + updatedAt: Date; +} + +class PermissionGroupSerialization extends PickType( + PermissionGetSerialization, + ['group'] as const +) { + @ApiProperty({ + type: () => PermissionGroupPermissionSerialization, + isArray: true, + }) + @Type(() => PermissionGroupPermissionSerialization) + permissions: PermissionGroupPermissionSerialization[]; +} + +export class PermissionGroupsSerialization { + @ApiProperty({ type: () => PermissionGroupSerialization, isArray: true }) + @Type(() => PermissionGroupSerialization) + groups: PermissionGroupSerialization[]; +} diff --git a/src/modules/permission/serializations/permission.list.serialization.ts b/src/modules/permission/serializations/permission.list.serialization.ts new file mode 100644 index 0000000..a63c64e --- /dev/null +++ b/src/modules/permission/serializations/permission.list.serialization.ts @@ -0,0 +1,3 @@ +import { PermissionGetSerialization } from './permission.get.serialization'; + +export class PermissionListSerialization extends PermissionGetSerialization {} diff --git a/src/modules/permission/services/permission.service.ts b/src/modules/permission/services/permission.service.ts new file mode 100644 index 0000000..0815373 --- /dev/null +++ b/src/modules/permission/services/permission.service.ts @@ -0,0 +1,171 @@ +import { Injectable } from '@nestjs/common'; +import { + IDatabaseCreateOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseCreateManyOptions, + IDatabaseManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; +import { PermissionCreateDto } from 'src/modules/permission/dtos/permission.create.dto'; +import { PermissionUpdateDescriptionDto } from 'src/modules/permission/dtos/permission.update-description.dto'; +import { PermissionUpdateGroupDto } from 'src/modules/permission/dtos/permission.update-group.dto'; +import { IPermissionGroup } from 'src/modules/permission/interfaces/permission.interface'; +import { IPermissionService } from 'src/modules/permission/interfaces/permission.service.interface'; +import { + PermissionDoc, + PermissionEntity, +} from 'src/modules/permission/repository/entities/permission.entity'; +import { PermissionRepository } from 'src/modules/permission/repository/repositories/permission.repository'; + +@Injectable() +export class PermissionService implements IPermissionService { + constructor(private readonly permissionRepository: PermissionRepository) {} + + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.permissionRepository.findAll(find, { + ...options + }); + } + + async findAllByIds( + ids: string[], + options?: IDatabaseFindAllOptions + ): Promise { + return this.permissionRepository.findAll( + { _id: { $in: ids } }, + options + ); + } + + async findAllByGroup( + filterGroups?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.permissionRepository.findAll( + { ...filterGroups }, + options + ); + } + + async findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.permissionRepository.findOneById(_id, { + ...options, + }); + } + + async findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + return this.permissionRepository.findOne(find, { + ...options, + }); + } + + async getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise { + return this.permissionRepository.getTotal(find, options); + } + + async delete(repository: PermissionDoc): Promise { + return this.permissionRepository.softDelete(repository); + } + + async create( + { group, code, description }: PermissionCreateDto, + options?: IDatabaseCreateOptions + ): Promise { + const create: PermissionEntity = new PermissionEntity(); + create.group = group; + create.code = code; + create.description = description ?? undefined; + create.isActive = true; + + return this.permissionRepository.create( + create, + options + ); + } + + async updateDescription( + repository: PermissionDoc, + { description }: PermissionUpdateDescriptionDto + ): Promise { + repository.description = description; + + return this.permissionRepository.save(repository); + } + + async updateGroup( + repository: PermissionDoc, + { group }: PermissionUpdateGroupDto + ): Promise { + repository.group = group; + + return this.permissionRepository.save(repository); + } + + async active(repository: PermissionDoc): Promise { + repository.isActive = true; + return this.permissionRepository.save(repository); + } + + async inactive(repository: PermissionDoc): Promise { + repository.isActive = false; + + return this.permissionRepository.save(repository); + } + + async groupingByGroups( + permissions: PermissionDoc[], + scope?: ENUM_PERMISSION_GROUP[] + ): Promise { + const permissionGroups: ENUM_PERMISSION_GROUP[] = + scope ??Object.values(ENUM_PERMISSION_GROUP); + const permissionEntity: PermissionEntity[] = permissions + .map((val) => val.toObject()) + .filter((val) => permissionGroups.includes(val.group)); + return permissionGroups.map((val) => ({ + group: val, + permissions: permissionEntity.filter((l) => l.group === val), + })); + } + + async createMany( + data: PermissionCreateDto[], + options?: IDatabaseCreateManyOptions + ): Promise { + const create: PermissionEntity[] = data.map((val) => { + const entity: PermissionEntity = new PermissionEntity(); + entity.code = val.code; + entity.description = val?.description; + entity.group = val.group; + entity.isActive = true; + + return entity; + }) as PermissionEntity[]; + + return this.permissionRepository.createMany( + create, + options + ); + } + + async deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise { + return this.permissionRepository.deleteMany(find, options); + } +} diff --git a/src/modules/role/constants/role.constant.ts b/src/modules/role/constants/role.constant.ts new file mode 100644 index 0000000..59d558e --- /dev/null +++ b/src/modules/role/constants/role.constant.ts @@ -0,0 +1 @@ +export const ROLE_ACTIVE_META_KEY = 'RoleActiveMetaKey'; diff --git a/src/modules/role/constants/role.doc.constant.ts b/src/modules/role/constants/role.doc.constant.ts new file mode 100644 index 0000000..84d082a --- /dev/null +++ b/src/modules/role/constants/role.doc.constant.ts @@ -0,0 +1,34 @@ +import { faker } from '@faker-js/faker'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; + +export const RoleDocQueryIsActive = [ + { + name: 'isActive', + allowEmptyValue: false, + required: true, + type: 'string', + example: 'true,false', + description: "boolean value with ',' delimiter", + }, +]; + +export const RoleDocQueryAccessFor = [ + { + name: 'accessFor', + allowEmptyValue: false, + required: true, + type: 'string', + example: Object.values(ENUM_AUTH_ACCESS_FOR).join(','), + description: "enum value with ',' delimiter", + }, +]; + +export const RoleDocParamsGet = [ + { + name: 'role', + allowEmptyValue: false, + required: true, + type: 'string', + example: faker.datatype.uuid(), + }, +]; diff --git a/src/modules/role/constants/role.list.constant.ts b/src/modules/role/constants/role.list.constant.ts new file mode 100644 index 0000000..35846b9 --- /dev/null +++ b/src/modules/role/constants/role.list.constant.ts @@ -0,0 +1,11 @@ +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; + +export const ROLE_DEFAULT_ORDER_BY = 'createdAt'; +export const ROLE_DEFAULT_ORDER_DIRECTION = + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC; +export const ROLE_DEFAULT_PER_PAGE = 20; +export const ROLE_DEFAULT_AVAILABLE_ORDER_BY = ['name', 'createdAt']; +export const ROLE_DEFAULT_AVAILABLE_SEARCH = ['name']; +export const ROLE_DEFAULT_IS_ACTIVE = [true, false]; +export const ROLE_DEFAULT_ACCESS_FOR = Object.values(ENUM_AUTH_ACCESS_FOR); diff --git a/src/modules/role/constants/role.status-code.constant.ts b/src/modules/role/constants/role.status-code.constant.ts new file mode 100644 index 0000000..1a17eb5 --- /dev/null +++ b/src/modules/role/constants/role.status-code.constant.ts @@ -0,0 +1,7 @@ +export enum ENUM_ROLE_STATUS_CODE_ERROR { + ROLE_NOT_FOUND_ERROR = 5500, + ROLE_EXIST_ERROR = 5501, + ROLE_IS_ACTIVE_ERROR = 5502, + ROLE_INACTIVE_ERROR = 5503, + ROLE_USED_ERROR = 5504, +} diff --git a/src/modules/role/controllers/role.admin.controller.ts b/src/modules/role/controllers/role.admin.controller.ts new file mode 100644 index 0000000..af48caa --- /dev/null +++ b/src/modules/role/controllers/role.admin.controller.ts @@ -0,0 +1,381 @@ +import { + Body, + ConflictException, + Controller, + Delete, + Get, + InternalServerErrorException, + NotFoundException, + Patch, + Post, + Put, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { AuthJwtAdminAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator'; +import { AuthPermissionProtected } from 'src/common/auth/decorators/auth.permission.decorator'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { + PaginationQuery, + PaginationQueryFilterInBoolean, + PaginationQueryFilterInEnum, +} from 'src/common/pagination/decorators/pagination.decorator'; +import { PaginationListDto } from 'src/common/pagination/dtos/pagination.list.dto'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { RequestParamGuard } from 'src/common/request/decorators/request.decorator'; +import { + Response, + ResponsePaging, +} from 'src/common/response/decorators/response.decorator'; +import { + IResponse, + IResponsePaging, +} from 'src/common/response/interfaces/response.interface'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { ENUM_PERMISSION_STATUS_CODE_ERROR } from 'src/modules/permission/constants/permission.status-code.constant'; +import { PermissionDoc, PermissionEntity } from "src/modules/permission/repository/entities/permission.entity"; +import { PermissionService } from 'src/modules/permission/services/permission.service'; +import { + ROLE_DEFAULT_ACCESS_FOR, + ROLE_DEFAULT_AVAILABLE_ORDER_BY, + ROLE_DEFAULT_AVAILABLE_SEARCH, + ROLE_DEFAULT_IS_ACTIVE, + ROLE_DEFAULT_ORDER_BY, + ROLE_DEFAULT_ORDER_DIRECTION, + ROLE_DEFAULT_PER_PAGE, +} from 'src/modules/role/constants/role.list.constant'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; +import { + RoleDeleteGuard, + RoleGetGuard, + RoleUpdateActiveGuard, + RoleUpdateGuard, + RoleUpdateInactiveGuard, +} from 'src/modules/role/decorators/role.admin.decorator'; +import { GetRole } from 'src/modules/role/decorators/role.decorator'; +import { + RoleAccessForDoc, + RoleActiveDoc, + RoleCreateDoc, + RoleDeleteDoc, + RoleGetDoc, + RoleInactiveDoc, + RoleListDoc, + RoleUpdateDoc, +} from 'src/modules/role/docs/role.admin.doc'; +import { RoleCreateDto } from 'src/modules/role/dtos/role.create.dto'; +import { RoleRequestDto } from 'src/modules/role/dtos/role.request.dto'; +import { RoleUpdateNameDto } from 'src/modules/role/dtos/role.update-name.dto'; +import { RoleUpdatePermissionDto } from 'src/modules/role/dtos/role.update-permission.dto'; +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; +import { RoleAccessForSerialization } from 'src/modules/role/serializations/role.access-for.serialization'; +import { RoleGetSerialization } from 'src/modules/role/serializations/role.get.serialization'; +import { RoleListSerialization } from 'src/modules/role/serializations/role.list.serialization'; +import { RoleService } from 'src/modules/role/services/role.service'; + +@ApiTags('modules.admin.role') +@Controller({ + version: '1', + path: '/role', +}) +export class RoleAdminController { + constructor( + private readonly paginationService: PaginationService, + private readonly permissionService: PermissionService, + private readonly roleService: RoleService + ) {} + + @RoleListDoc() + @ResponsePaging('role.list', { + serialization: RoleListSerialization, + }) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.ROLE_READ) + @AuthJwtAdminAccessProtected() + @Get('/list') + async list( + @PaginationQuery( + ROLE_DEFAULT_PER_PAGE, + ROLE_DEFAULT_ORDER_BY, + ROLE_DEFAULT_ORDER_DIRECTION, + ROLE_DEFAULT_AVAILABLE_SEARCH, + ROLE_DEFAULT_AVAILABLE_ORDER_BY + ) + { _search, _limit, _offset, _order }: PaginationListDto, + @PaginationQueryFilterInBoolean('isActive', ROLE_DEFAULT_IS_ACTIVE) + isActive: Record, + @PaginationQueryFilterInEnum( + 'accessFor', + ROLE_DEFAULT_ACCESS_FOR, + ENUM_AUTH_ACCESS_FOR + ) + accessFor: Record + ): Promise { + const find: Record = { + ..._search, + ...isActive, + ...accessFor, + }; + + const roles: RoleEntity[] = await this.roleService.findAll(find, { + paging: { + limit: _limit, + offset: _offset, + }, + order: _order, + }); + + const total: number = await this.roleService.getTotal(find); + const totalPage: number = this.paginationService.totalPage( + total, + _limit + ); + + return { + _pagination: { total, totalPage }, + data: roles, + }; + } + + @RoleGetDoc() + @Response('role.get', { + serialization: RoleGetSerialization, + }) + @RoleGetGuard() + @RequestParamGuard(RoleRequestDto) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.ROLE_READ) + @AuthJwtAdminAccessProtected() + @Get('get/:role') + async get(@GetRole(true) role: RoleEntity): Promise { + const permissions: PermissionEntity[] = + await this.permissionService.findAllByIds(role.permissions); + return { data: { ...role, permissions } }; + } + + @RoleCreateDoc() + @Response('role.create', { + serialization: ResponseIdSerialization, + }) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.ROLE_READ, + ENUM_AUTH_PERMISSIONS.ROLE_CREATE + ) + @AuthJwtAdminAccessProtected() + @Post('/create') + async create( + @Body() + { name, permissions, accessFor }: RoleCreateDto + ): Promise { + const exist: boolean = await this.roleService.existByName(name); + if (exist) { + throw new ConflictException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_EXIST_ERROR, + message: 'role.error.exist', + }); + } + + const permissionsCheck: PermissionDoc[] = + await this.permissionService.findAllByIds(permissions); + if (permissionsCheck.length !== permissions.length) { + throw new NotFoundException({ + statusCode: + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR, + message: 'permission.error.notFound', + }); + } + + try { + const create = await this.roleService.create({ + name, + permissions, + accessFor, + }); + + return { + data: { _id: create._id }, + }; + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + } + + @RoleUpdateDoc() + @Response('role.update', { + serialization: ResponseIdSerialization, + }) + @RoleUpdateGuard() + @RequestParamGuard(RoleRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.ROLE_READ, + ENUM_AUTH_PERMISSIONS.ROLE_UPDATE + ) + @AuthJwtAdminAccessProtected() + @Put('/update/:role') + async update( + @GetRole() role: RoleDoc, + @Body() + { name }: RoleUpdateNameDto + ): Promise { + const check: boolean = await this.roleService.existByName(name, { + excludeId: [role._id], + }); + if (check) { + throw new ConflictException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_EXIST_ERROR, + message: 'role.error.exist', + }); + } + + try { + await this.roleService.updateName(role, { name }); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return { + data: { _id: role._id }, + }; + } + + @RoleUpdateDoc() + @Response('role.updatePermission', { + serialization: ResponseIdSerialization, + }) + @RoleUpdateGuard() + @RequestParamGuard(RoleRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.ROLE_READ, + ENUM_AUTH_PERMISSIONS.ROLE_UPDATE + ) + @AuthJwtAdminAccessProtected() + @Put('/update/:role/permission') + async updatePermission( + @GetRole() role: RoleDoc, + @Body() + { accessFor, permissions }: RoleUpdatePermissionDto + ): Promise { + const permissionsCheck: PermissionDoc[] = + await this.permissionService.findAllByIds(permissions); + + if (permissionsCheck.length !== permissions.length) { + throw new NotFoundException({ + statusCode: + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR, + message: 'permission.error.notFound', + }); + } + + try { + await this.roleService.updatePermission(role, { + accessFor, + permissions, + }); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return { + data: { _id: role._id }, + }; + } + + @RoleDeleteDoc() + @Response('role.delete') + @RoleDeleteGuard() + @RequestParamGuard(RoleRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.ROLE_READ, + ENUM_AUTH_PERMISSIONS.ROLE_DELETE + ) + @AuthJwtAdminAccessProtected() + @Delete('/delete/:role') + async delete(@GetRole() role: RoleDoc): Promise { + try { + await this.roleService.delete(role); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @RoleInactiveDoc() + @Response('role.inactive') + @RoleUpdateInactiveGuard() + @RequestParamGuard(RoleRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.ROLE_READ, + ENUM_AUTH_PERMISSIONS.ROLE_UPDATE, + ENUM_AUTH_PERMISSIONS.ROLE_INACTIVE + ) + @AuthJwtAdminAccessProtected() + @Patch('/update/:role/inactive') + async inactive(@GetRole() role: RoleDoc): Promise { + try { + await this.roleService.inactive(role); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @RoleActiveDoc() + @Response('role.active') + @RoleUpdateActiveGuard() + @RequestParamGuard(RoleRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.ROLE_READ, + ENUM_AUTH_PERMISSIONS.ROLE_UPDATE, + ENUM_AUTH_PERMISSIONS.ROLE_ACTIVE + ) + @AuthJwtAdminAccessProtected() + @Patch('/update/:role/active') + async active(@GetRole() role: RoleDoc): Promise { + try { + await this.roleService.active(role); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @RoleAccessForDoc() + @Response('role.accessFor', { serialization: RoleAccessForSerialization }) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.ROLE_READ) + @AuthJwtAdminAccessProtected() + @Get('/access-for') + async accessFor(): Promise { + const accessFor: string[] = await this.roleService.getAccessFor(); + + return { data: { accessFor } }; + } +} diff --git a/src/modules/role/decorators/role.admin.decorator.ts b/src/modules/role/decorators/role.admin.decorator.ts new file mode 100644 index 0000000..9572403 --- /dev/null +++ b/src/modules/role/decorators/role.admin.decorator.ts @@ -0,0 +1,48 @@ +import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'; +import { ROLE_ACTIVE_META_KEY } from 'src/modules/role/constants/role.constant'; +import { RoleActiveGuard } from 'src/modules/role/guards/role.active.guard'; +import { RoleNotFoundGuard } from 'src/modules/role/guards/role.not-found.guard'; +import { RolePutToRequestGuard } from 'src/modules/role/guards/role.put-to-request.guard'; +import { RoleUsedGuard } from 'src/modules/role/guards/role.used.guard'; + +export function RoleGetGuard(): MethodDecorator { + return applyDecorators(UseGuards(RolePutToRequestGuard, RoleNotFoundGuard)); +} + +export function RoleUpdateGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + RolePutToRequestGuard, + RoleNotFoundGuard, + RoleActiveGuard, + RoleUsedGuard + ), + SetMetadata(ROLE_ACTIVE_META_KEY, [true]) + ); +} + +export function RoleDeleteGuard(): MethodDecorator { + return applyDecorators( + UseGuards( + RolePutToRequestGuard, + RoleNotFoundGuard, + RoleActiveGuard, + RoleUsedGuard + ), + SetMetadata(ROLE_ACTIVE_META_KEY, [true]) + ); +} + +export function RoleUpdateActiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards(RolePutToRequestGuard, RoleNotFoundGuard, RoleActiveGuard), + SetMetadata(ROLE_ACTIVE_META_KEY, [false]) + ); +} + +export function RoleUpdateInactiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards(RolePutToRequestGuard, RoleNotFoundGuard, RoleActiveGuard), + SetMetadata(ROLE_ACTIVE_META_KEY, [true]) + ); +} diff --git a/src/modules/role/decorators/role.decorator.ts b/src/modules/role/decorators/role.decorator.ts new file mode 100644 index 0000000..260b4b1 --- /dev/null +++ b/src/modules/role/decorators/role.decorator.ts @@ -0,0 +1,12 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; + +export const GetRole = createParamDecorator( + (returnPlain: boolean, ctx: ExecutionContext): RoleDoc | RoleEntity => { + const { __role } = ctx.switchToHttp().getRequest(); + return returnPlain ? __role.toObject() : __role; + } +); diff --git a/src/modules/role/docs/role.admin.doc.ts b/src/modules/role/docs/role.admin.doc.ts new file mode 100644 index 0000000..317d008 --- /dev/null +++ b/src/modules/role/docs/role.admin.doc.ts @@ -0,0 +1,127 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { Doc, DocPaging } from 'src/common/doc/decorators/doc.decorator'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { + RoleDocParamsGet, + RoleDocQueryAccessFor, + RoleDocQueryIsActive, +} from 'src/modules/role/constants/role.doc.constant'; +import { RoleAccessForSerialization } from 'src/modules/role/serializations/role.access-for.serialization'; +import { RoleGetSerialization } from 'src/modules/role/serializations/role.get.serialization'; +import { RoleListSerialization } from 'src/modules/role/serializations/role.list.serialization'; + +export function RoleListDoc(): MethodDecorator { + return applyDecorators( + DocPaging('role.list', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + queries: [...RoleDocQueryIsActive, ...RoleDocQueryAccessFor], + }, + response: { + serialization: RoleListSerialization, + }, + }) + ); +} + +export function RoleGetDoc(): MethodDecorator { + return applyDecorators( + Doc('role.get', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: RoleDocParamsGet, + }, + response: { serialization: RoleGetSerialization }, + }) + ); +} + +export function RoleCreateDoc(): MethodDecorator { + return applyDecorators( + Doc('role.create', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + response: { + httpStatus: HttpStatus.CREATED, + serialization: ResponseIdSerialization, + }, + }) + ); +} + +export function RoleUpdateDoc(): MethodDecorator { + return applyDecorators( + Doc('role.update', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: RoleDocParamsGet, + }, + response: { serialization: ResponseIdSerialization }, + }) + ); +} + +export function RoleDeleteDoc(): MethodDecorator { + return applyDecorators( + Doc('role.delete', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: RoleDocParamsGet, + }, + }) + ); +} + +export function RoleActiveDoc(): MethodDecorator { + return applyDecorators( + Doc('role.active', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: RoleDocParamsGet, + }, + }) + ); +} + +export function RoleInactiveDoc(): MethodDecorator { + return applyDecorators( + Doc('role.inactive', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: RoleDocParamsGet, + }, + }) + ); +} + +export function RoleAccessForDoc(): MethodDecorator { + return applyDecorators( + Doc('role.accessFor', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + response: { serialization: RoleAccessForSerialization }, + }) + ); +} diff --git a/src/modules/role/dtos/role.active.dto.ts b/src/modules/role/dtos/role.active.dto.ts new file mode 100644 index 0000000..dfcf6f7 --- /dev/null +++ b/src/modules/role/dtos/role.active.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsBoolean } from 'class-validator'; + +export class RoleActiveDto { + @ApiProperty({ + name: 'isActive', + description: 'is active role', + required: true, + nullable: false, + }) + @IsBoolean() + @IsNotEmpty() + isActive: boolean; +} diff --git a/src/modules/role/dtos/role.create.dto.ts b/src/modules/role/dtos/role.create.dto.ts new file mode 100644 index 0000000..8d40fef --- /dev/null +++ b/src/modules/role/dtos/role.create.dto.ts @@ -0,0 +1,46 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsString, + IsNotEmpty, + MaxLength, + MinLength, + IsEnum, + IsArray, + IsUUID, +} from 'class-validator'; +import { ENUM_AUTH_ACCESS_FOR_DEFAULT } from 'src/common/auth/constants/auth.enum.constant'; + +export class RoleCreateDto { + @ApiProperty({ + description: 'Alias name of role', + example: faker.name.jobTitle(), + required: true, + }) + @IsString() + @IsNotEmpty() + @MinLength(3) + @MaxLength(30) + @Type(() => String) + readonly name: string; + + @ApiProperty({ + description: 'List of permission', + example: [faker.datatype.uuid(), faker.datatype.uuid()], + required: true, + }) + @IsUUID('4', { each: true }) + @IsArray() + @IsNotEmpty() + readonly permissions: string[]; + + @ApiProperty({ + description: 'Representative for role', + example: 'ADMIN', + required: true, + }) + @IsEnum(ENUM_AUTH_ACCESS_FOR_DEFAULT) + @IsNotEmpty() + readonly accessFor: ENUM_AUTH_ACCESS_FOR_DEFAULT; +} diff --git a/src/modules/role/dtos/role.request.dto.ts b/src/modules/role/dtos/role.request.dto.ts new file mode 100644 index 0000000..2c9eb6f --- /dev/null +++ b/src/modules/role/dtos/role.request.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class RoleRequestDto { + @ApiProperty({ + name: 'role', + description: 'role id', + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsUUID('4') + @Type(() => String) + role: string; +} diff --git a/src/modules/role/dtos/role.update-name.dto.ts b/src/modules/role/dtos/role.update-name.dto.ts new file mode 100644 index 0000000..a9b56da --- /dev/null +++ b/src/modules/role/dtos/role.update-name.dto.ts @@ -0,0 +1,6 @@ +import { PickType } from '@nestjs/swagger'; +import { RoleCreateDto } from './role.create.dto'; + +export class RoleUpdateNameDto extends PickType(RoleCreateDto, [ + 'name', +] as const) {} diff --git a/src/modules/role/dtos/role.update-permission.dto.ts b/src/modules/role/dtos/role.update-permission.dto.ts new file mode 100644 index 0000000..abbe963 --- /dev/null +++ b/src/modules/role/dtos/role.update-permission.dto.ts @@ -0,0 +1,7 @@ +import { RoleCreateDto } from './role.create.dto'; +import { PickType } from '@nestjs/swagger'; + +export class RoleUpdatePermissionDto extends PickType(RoleCreateDto, [ + 'accessFor', + 'permissions', +] as const) {} diff --git a/src/modules/role/guards/role.active.guard.ts b/src/modules/role/guards/role.active.guard.ts new file mode 100644 index 0000000..f326af8 --- /dev/null +++ b/src/modules/role/guards/role.active.guard.ts @@ -0,0 +1,35 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ROLE_ACTIVE_META_KEY } from 'src/modules/role/constants/role.constant'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; + +@Injectable() +export class RoleActiveGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const required: boolean[] = this.reflector.getAllAndOverride( + ROLE_ACTIVE_META_KEY, + [context.getHandler(), context.getClass()] + ); + + if (!required) { + return true; + } + + const { __role } = context.switchToHttp().getRequest(); + + if (!required.includes(__role.isActive)) { + throw new BadRequestException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_IS_ACTIVE_ERROR, + message: 'role.error.isActiveInvalid', + }); + } + return true; + } +} diff --git a/src/modules/role/guards/role.not-found.guard.ts b/src/modules/role/guards/role.not-found.guard.ts new file mode 100644 index 0000000..fc67b02 --- /dev/null +++ b/src/modules/role/guards/role.not-found.guard.ts @@ -0,0 +1,23 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + NotFoundException, +} from '@nestjs/common'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; + +@Injectable() +export class RoleNotFoundGuard implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + const { __role } = context.switchToHttp().getRequest(); + + if (!__role) { + throw new NotFoundException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR, + message: 'role.error.notFound', + }); + } + + return true; + } +} diff --git a/src/modules/role/guards/role.put-to-request.guard.ts b/src/modules/role/guards/role.put-to-request.guard.ts new file mode 100644 index 0000000..9098aa0 --- /dev/null +++ b/src/modules/role/guards/role.put-to-request.guard.ts @@ -0,0 +1,21 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { RoleService } from 'src/modules/role/services/role.service'; + +@Injectable() +export class RolePutToRequestGuard implements CanActivate { + constructor(private readonly roleService: RoleService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { params } = request; + const { role } = params; + + const check: RoleDoc = await this.roleService.findOneById(role, { + join: true, + }); + request.__role = check; + + return true; + } +} diff --git a/src/modules/role/guards/role.used.guard.ts b/src/modules/role/guards/role.used.guard.ts new file mode 100644 index 0000000..613a087 --- /dev/null +++ b/src/modules/role/guards/role.used.guard.ts @@ -0,0 +1,32 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; +import { UserDoc } from "src/modules/user/repository/entities/user.entity"; +import { UserService } from 'src/modules/user/services/user.service'; + +@Injectable() +export class RoleUsedGuard implements CanActivate { + constructor(private readonly userService: UserService) {} + + async canActivate(context: ExecutionContext): Promise { + const { __role } = context.switchToHttp().getRequest(); + const check: UserDoc = await this.userService.findOne( + { + role: __role._id, + } + ); + + if (check) { + throw new BadRequestException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_USED_ERROR, + message: 'role.error.used', + }); + } + + return true; + } +} diff --git a/src/modules/role/interfaces/role.interface.ts b/src/modules/role/interfaces/role.interface.ts new file mode 100644 index 0000000..9fb70c2 --- /dev/null +++ b/src/modules/role/interfaces/role.interface.ts @@ -0,0 +1,16 @@ +import { + PermissionDoc, + PermissionEntity, +} from 'src/modules/permission/repository/entities/permission.entity'; +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; + +export interface IRoleEntity extends Omit { + permissions: PermissionEntity[]; +} + +export interface IRoleDoc extends Omit { + permissions: PermissionDoc[]; +} diff --git a/src/modules/role/interfaces/role.service.interface.ts b/src/modules/role/interfaces/role.service.interface.ts new file mode 100644 index 0000000..8203757 --- /dev/null +++ b/src/modules/role/interfaces/role.service.interface.ts @@ -0,0 +1,87 @@ +import { + IDatabaseCreateOptions, + IDatabaseExistOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseManyOptions, + IDatabaseCreateManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { RoleCreateDto } from 'src/modules/role/dtos/role.create.dto'; +import { RoleUpdateNameDto } from 'src/modules/role/dtos/role.update-name.dto'; +import { RoleUpdatePermissionDto } from 'src/modules/role/dtos/role.update-permission.dto'; +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; +import { IRoleDoc } from "./role.interface"; + +export interface IRoleService { + findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + + findOneById(_id: string, options?: IDatabaseFindOneOptions): Promise; + + findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise; + + findOneByName( + name: string, + options?: IDatabaseFindOneOptions + ): Promise; + + getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise; + + exist(_id: string, options?: IDatabaseExistOptions): Promise; + + existByName( + name: string, + options?: IDatabaseExistOptions + ): Promise; + + create( + {accessFor, permissions, name}: RoleCreateDto, + options?: IDatabaseCreateOptions + ): Promise; + + createSuperAdmin(options?: IDatabaseCreateOptions): Promise; + + updateName( + repository: RoleDoc, + {name}: RoleUpdateNameDto + ): Promise; + + updatePermission( + repository: RoleDoc, + {accessFor, permissions}: RoleUpdatePermissionDto + ): Promise; + + active(repository: RoleDoc): Promise; + + inactive(repository: RoleDoc): Promise; + + joinWithPermission(repository: RoleDoc): Promise; + + delete(repository: RoleDoc): Promise; + + deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise; + + createMany( + data: RoleCreateDto[], + options?: IDatabaseCreateManyOptions + ): Promise; + + getAccessFor(): Promise; +} diff --git a/src/modules/role/repository/entities/role.entity.ts b/src/modules/role/repository/entities/role.entity.ts new file mode 100644 index 0000000..744c0e5 --- /dev/null +++ b/src/modules/role/repository/entities/role.entity.ts @@ -0,0 +1,57 @@ +import { Prop, SchemaFactory } from '@nestjs/mongoose'; +import { CallbackWithoutResultAndOptionalError, Document } from 'mongoose'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntity } from 'src/common/database/decorators/database.decorator'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; + +export const RoleDatabaseName = 'roles'; + +@DatabaseEntity({ collection: RoleDatabaseName }) +export class RoleEntity extends DatabaseMongoUUIDEntityAbstract { + @Prop({ + required: true, + index: true, + unique: true, + lowercase: true, + trim: true, + maxlength: 100, + type: String, + }) + name: string; + + @Prop({ + required: true, + default: [], + _id: false, + type: Array, + ref: PermissionEntity.name, + }) + permissions: string[]; + + @Prop({ + required: true, + default: true, + index: true, + type: Boolean, + }) + isActive: boolean; + + @Prop({ + required: true, + enum: ENUM_AUTH_ACCESS_FOR, + index: true, + type: String, + }) + accessFor: ENUM_AUTH_ACCESS_FOR; +} + +export const RoleSchema = SchemaFactory.createForClass(RoleEntity); + +export type RoleDoc = RoleEntity & Document; + +RoleSchema.pre('save', function (next: CallbackWithoutResultAndOptionalError) { + this.name = this.name.toLowerCase(); + + next(); +}); diff --git a/src/modules/role/repository/repositories/role.repository.ts b/src/modules/role/repository/repositories/role.repository.ts new file mode 100644 index 0000000..0dcd331 --- /dev/null +++ b/src/modules/role/repository/repositories/role.repository.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { Model } from 'mongoose'; +import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; +import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; + +@Injectable() +export class RoleRepository extends DatabaseMongoUUIDRepositoryAbstract< + RoleEntity, + RoleDoc +> { + constructor( + @DatabaseModel(RoleEntity.name) + private readonly roleModel: Model + ) { + super(roleModel, { + path: 'permissions', + localField: 'permissions', + foreignField: '_id', + model: PermissionEntity.name, + }); + } +} diff --git a/src/modules/role/repository/role.repository.module.ts b/src/modules/role/repository/role.repository.module.ts new file mode 100644 index 0000000..169e73b --- /dev/null +++ b/src/modules/role/repository/role.repository.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { + RoleEntity, + RoleSchema, +} from 'src/modules/role/repository/entities/role.entity'; +import { RoleRepository } from 'src/modules/role/repository/repositories/role.repository'; + +@Module({ + providers: [RoleRepository], + exports: [RoleRepository], + controllers: [], + imports: [ + MongooseModule.forFeature( + [ + { + name: RoleEntity.name, + schema: RoleSchema, + }, + ], + DATABASE_CONNECTION_NAME + ), + ], +}) +export class RoleRepositoryModule {} diff --git a/src/modules/role/role.module.ts b/src/modules/role/role.module.ts new file mode 100644 index 0000000..b275938 --- /dev/null +++ b/src/modules/role/role.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { RoleRepositoryModule } from 'src/modules/role/repository/role.repository.module'; +import { RoleService } from './services/role.service'; + +@Module({ + controllers: [], + providers: [RoleService], + exports: [RoleService], + imports: [RoleRepositoryModule], +}) +export class RoleModule {} diff --git a/src/modules/role/serializations/role.access-for.serialization.ts b/src/modules/role/serializations/role.access-for.serialization.ts new file mode 100644 index 0000000..236e161 --- /dev/null +++ b/src/modules/role/serializations/role.access-for.serialization.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; + +export class RoleAccessForSerialization { + @ApiProperty({ + description: 'Access for role', + example: [ENUM_AUTH_ACCESS_FOR.USER, ENUM_AUTH_ACCESS_FOR.ADMIN], + required: true, + }) + groups: string[]; +} diff --git a/src/modules/role/serializations/role.get.serialization.ts b/src/modules/role/serializations/role.get.serialization.ts new file mode 100644 index 0000000..c086fcd --- /dev/null +++ b/src/modules/role/serializations/role.get.serialization.ts @@ -0,0 +1,55 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Exclude, Type } from 'class-transformer'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { PermissionGetSerialization } from 'src/modules/permission/serializations/permission.get.serialization'; + +export class RoleGetSerialization extends ResponseIdSerialization { + @ApiProperty({ + description: 'Active flag of role', + example: true, + required: true, + }) + readonly isActive: boolean; + + @ApiProperty({ + description: 'Alias name of role', + example: faker.name.jobTitle(), + required: true, + }) + readonly name: string; + + @ApiProperty({ + description: 'Representative for role', + example: 'ADMIN', + required: true, + }) + readonly accessFor: ENUM_AUTH_ACCESS_FOR; + + @ApiProperty({ + description: 'List of permission', + type: () => PermissionGetSerialization, + isArray: true, + required: true, + }) + @Type(() => PermissionGetSerialization) + readonly permissions: PermissionGetSerialization[]; + + @ApiProperty({ + description: 'Date created at', + example: faker.date.recent(), + required: true, + }) + readonly createdAt: Date; + + @ApiProperty({ + description: 'Date updated at', + example: faker.date.recent(), + required: false, + }) + readonly updatedAt: Date; + + @Exclude() + readonly deletedAt?: Date; +} diff --git a/src/modules/role/serializations/role.list.serialization.ts b/src/modules/role/serializations/role.list.serialization.ts new file mode 100644 index 0000000..a8afd7d --- /dev/null +++ b/src/modules/role/serializations/role.list.serialization.ts @@ -0,0 +1,16 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { RoleGetSerialization } from './role.get.serialization'; + +export class RoleListSerialization extends OmitType(RoleGetSerialization, [ + 'permissions', +] as const) { + @ApiProperty({ + description: 'Count of permissions', + example: faker.random.numeric(2, { allowLeadingZeros: false }), + required: true, + }) + @Transform(({ value }) => value.length) + readonly permissions: number; +} diff --git a/src/modules/role/services/role.service.ts b/src/modules/role/services/role.service.ts new file mode 100644 index 0000000..6daeca4 --- /dev/null +++ b/src/modules/role/services/role.service.ts @@ -0,0 +1,190 @@ +import { Injectable } from '@nestjs/common'; +import { + ENUM_AUTH_ACCESS_FOR, + ENUM_AUTH_ACCESS_FOR_DEFAULT, +} from 'src/common/auth/constants/auth.enum.constant'; +import { + IDatabaseCreateOptions, + IDatabaseExistOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseManyOptions, + IDatabaseCreateManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { RoleCreateDto } from 'src/modules/role/dtos/role.create.dto'; +import { RoleUpdateNameDto } from 'src/modules/role/dtos/role.update-name.dto'; +import { RoleUpdatePermissionDto } from 'src/modules/role/dtos/role.update-permission.dto'; +import { IRoleService } from 'src/modules/role/interfaces/role.service.interface'; +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; +import { RoleRepository } from 'src/modules/role/repository/repositories/role.repository'; +import { IRoleDoc } from "../interfaces/role.interface"; + +@Injectable() +export class RoleService implements IRoleService { + constructor(private readonly roleRepository: RoleRepository) {} + + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.roleRepository.findAll(find, { + ...options, + join: false, + }); + } + + async findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.roleRepository.findOneById(_id, options); + } + + async findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + return this.roleRepository.findOne(find, options); + } + + async findOneByName( + name: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.roleRepository.findOne({ name }, options); + } + + async getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise { + return this.roleRepository.getTotal(find, options); + } + + async exist( + _id: string, + options?: IDatabaseExistOptions + ): Promise { + return this.roleRepository.exists( + { + _id, + }, + options + ); + } + + async existByName( + name: string, + options?: IDatabaseExistOptions + ): Promise { + return this.roleRepository.exists( + { + name, + }, + { ...options, withDeleted: true } + ); + } + + async create( + { accessFor, permissions, name }: RoleCreateDto, + options?: IDatabaseCreateOptions + ): Promise { + const create: RoleEntity = new RoleEntity(); + create.accessFor = accessFor; + create.permissions = permissions; + create.isActive = true; + create.name = name; + + return this.roleRepository.create(create, options); + } + + async createSuperAdmin( + options?: IDatabaseCreateOptions + ): Promise { + const create: RoleEntity = new RoleEntity(); + create.name = 'superadmin'; + create.permissions = []; + create.isActive = true; + create.accessFor = ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN; + + return this.roleRepository.create(create, options); + } + + async updateName( + repository: RoleDoc, + { name }: RoleUpdateNameDto + ): Promise { + repository.name = name; + + return this.roleRepository.save(repository); + } + + async joinWithPermission(repository: RoleDoc): Promise { + return repository.populate({ + path: 'permissions', + localField: 'permissions', + foreignField: '_id', + model: PermissionEntity.name, + }); + } + + async updatePermission( + repository: RoleDoc, + { accessFor, permissions }: RoleUpdatePermissionDto + ): Promise { + repository.accessFor = accessFor; + repository.permissions = permissions; + + return this.roleRepository.save(repository); + } + + async active(repository: RoleDoc): Promise { + repository.isActive = true; + + return this.roleRepository.save(repository); + } + + async inactive(repository: RoleDoc): Promise { + repository.isActive = false; + + return this.roleRepository.save(repository); + } + + async delete(repository: RoleDoc): Promise { + return this.roleRepository.softDelete(repository); + } + + async deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise { + return this.roleRepository.deleteMany(find, options); + } + + async createMany( + data: RoleCreateDto[], + options?: IDatabaseCreateManyOptions + ): Promise { + const create: RoleEntity[] = data.map( + ({ accessFor, permissions, name }) => { + const entity: RoleEntity = new RoleEntity(); + entity.accessFor = accessFor; + entity.permissions = permissions; + entity.isActive = true; + entity.name = name; + + return entity; + } + ); + return this.roleRepository.createMany(create, options); + } + + async getAccessFor(): Promise { + return Object.values(ENUM_AUTH_ACCESS_FOR_DEFAULT); + } +} diff --git a/src/modules/user/constants/user.constant.ts b/src/modules/user/constants/user.constant.ts new file mode 100644 index 0000000..cad926c --- /dev/null +++ b/src/modules/user/constants/user.constant.ts @@ -0,0 +1,2 @@ +export const USER_ACTIVE_META_KEY = 'UserActiveMetaKey'; +export const USER_BLOCKED_META_KEY = 'UserBlockedMetaKey'; diff --git a/src/modules/user/constants/user.doc.constant.ts b/src/modules/user/constants/user.doc.constant.ts new file mode 100644 index 0000000..9a71784 --- /dev/null +++ b/src/modules/user/constants/user.doc.constant.ts @@ -0,0 +1,33 @@ +import { faker } from '@faker-js/faker'; + +export const UserDocQueryIsActive = [ + { + name: 'isActive', + allowEmptyValue: false, + required: true, + type: 'string', + example: 'true,false', + description: "boolean value with ',' delimiter", + }, +]; + +export const UserDocQueryBlocked = [ + { + name: 'blocked', + allowEmptyValue: false, + required: true, + type: 'string', + example: 'true,false', + description: "boolean value with ',' delimiter", + }, +]; + +export const UserDocParamsGet = [ + { + name: 'user', + allowEmptyValue: false, + required: true, + type: 'string', + example: faker.datatype.uuid(), + }, +]; diff --git a/src/modules/user/constants/user.list.constant.ts b/src/modules/user/constants/user.list.constant.ts new file mode 100644 index 0000000..964f96b --- /dev/null +++ b/src/modules/user/constants/user.list.constant.ts @@ -0,0 +1,24 @@ +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; + +export const USER_DEFAULT_PER_PAGE = 20; +export const USER_DEFAULT_ORDER_BY = 'createdAt'; +export const USER_DEFAULT_ORDER_DIRECTION = + ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC; +export const USER_DEFAULT_AVAILABLE_ORDER_BY = [ + 'username', + 'firstName', + 'lastName', + 'email', + 'mobileNumber', + 'createdAt', +]; +export const USER_DEFAULT_AVAILABLE_SEARCH = [ + 'username', + 'firstName', + 'lastName', + 'email', + 'mobileNumber', +]; + +export const USER_DEFAULT_IS_ACTIVE = [true, false]; +export const USER_DEFAULT_BLOCKED = [true, false]; diff --git a/src/modules/user/constants/user.status-code.constant.ts b/src/modules/user/constants/user.status-code.constant.ts new file mode 100644 index 0000000..4cd69f4 --- /dev/null +++ b/src/modules/user/constants/user.status-code.constant.ts @@ -0,0 +1,13 @@ +export enum ENUM_USER_STATUS_CODE_ERROR { + USER_NOT_FOUND_ERROR = 5400, + USER_USERNAME_EXISTS_ERROR = 5401, + USER_EMAIL_EXIST_ERROR = 5402, + USER_MOBILE_NUMBER_EXIST_ERROR = 5403, + USER_IS_ACTIVE_ERROR = 5404, + USER_INACTIVE_ERROR = 5405, + USER_PASSWORD_NOT_MATCH_ERROR = 5406, + USER_PASSWORD_NEW_MUST_DIFFERENCE_ERROR = 5407, + USER_PASSWORD_EXPIRED_ERROR = 5408, + USER_PASSWORD_ATTEMPT_MAX_ERROR = 5409, + USER_BLOCKED_ERROR = 5410, +} diff --git a/src/modules/user/controllers/user.admin.controller.ts b/src/modules/user/controllers/user.admin.controller.ts new file mode 100644 index 0000000..0d10dec --- /dev/null +++ b/src/modules/user/controllers/user.admin.controller.ts @@ -0,0 +1,414 @@ +import { + Controller, + Get, + Post, + Body, + Delete, + Put, + InternalServerErrorException, + NotFoundException, + UploadedFile, + ConflictException, + Patch, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { UploadFileSingle } from 'src/common/file/decorators/file.decorator'; +import { IFileExtract } from 'src/common/file/interfaces/file.interface'; +import { FileExtractPipe } from 'src/common/file/pipes/file.extract.pipe'; +import { FileRequiredPipe } from 'src/common/file/pipes/file.required.pipe'; +import { FileSizeExcelPipe } from 'src/common/file/pipes/file.size.pipe'; +import { FileTypeExcelPipe } from 'src/common/file/pipes/file.type.pipe'; +import { FileValidationPipe } from 'src/common/file/pipes/file.validation.pipe'; +import { ENUM_HELPER_FILE_TYPE } from 'src/common/helper/constants/helper.enum.constant'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import { RequestParamGuard } from 'src/common/request/decorators/request.decorator'; +import { + Response, + ResponseExcel, + ResponsePaging, +} from 'src/common/response/decorators/response.decorator'; +import { + IResponse, + IResponsePaging, +} from 'src/common/response/interfaces/response.interface'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { + UserDeleteGuard, + UserGetGuard, + UserUpdateActiveGuard, + UserUpdateBlockedGuard, + UserUpdateGuard, + UserUpdateInactiveGuard, +} from 'src/modules/user/decorators/user.admin.decorator'; +import { GetUser } from 'src/modules/user/decorators/user.decorator'; +import { + UserActiveDoc, + UserBlockedDoc, + UserCreateDoc, + UserDeleteDoc, + UserExportDoc, + UserGetDoc, + UserImportDoc, + UserInactiveDoc, + UserListDoc, + UserUpdateDoc, +} from 'src/modules/user/docs/user.admin.doc'; +import { UserCreateDto } from 'src/modules/user/dtos/user.create.dto'; +import { UserImportDto } from 'src/modules/user/dtos/user.import.dto'; +import { UserRequestDto } from 'src/modules/user/dtos/user.request.dto'; +import { IUserDoc, IUserEntity } from "src/modules/user/interfaces/user.interface"; +import { UserGetSerialization } from 'src/modules/user/serializations/user.get.serialization'; +import { UserImportSerialization } from 'src/modules/user/serializations/user.import.serialization'; +import { UserListSerialization } from 'src/modules/user/serializations/user.list.serialization'; +import { UserService } from 'src/modules/user/services/user.service'; +import { AuthJwtAdminAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator'; +import { AuthPermissionProtected } from 'src/common/auth/decorators/auth.permission.decorator'; +import { UserUpdateNameDto } from 'src/modules/user/dtos/user.update-name.dto'; +import { + USER_DEFAULT_AVAILABLE_ORDER_BY, + USER_DEFAULT_AVAILABLE_SEARCH, + USER_DEFAULT_BLOCKED, + USER_DEFAULT_IS_ACTIVE, + USER_DEFAULT_ORDER_BY, + USER_DEFAULT_ORDER_DIRECTION, + USER_DEFAULT_PER_PAGE, +} from 'src/modules/user/constants/user.list.constant'; +import { PaginationListDto } from 'src/common/pagination/dtos/pagination.list.dto'; +import { + PaginationQuery, + PaginationQueryFilterInBoolean, +} from 'src/common/pagination/decorators/pagination.decorator'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { IAuthPassword } from "../../../common/auth/interfaces/auth.interface"; + +@ApiTags('modules.admin.user') +@Controller({ + version: '1', + path: '/user', +}) +export class UserAdminController { + constructor( + private readonly authService: AuthService, + private readonly paginationService: PaginationService, + private readonly userService: UserService, + private readonly roleService: RoleService + ) {} + + @UserListDoc() + @ResponsePaging('user.list', { + serialization: UserListSerialization, + }) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.USER_READ) + @AuthJwtAdminAccessProtected() + @Get('/list') + async list( + @PaginationQuery( + USER_DEFAULT_PER_PAGE, + USER_DEFAULT_ORDER_BY, + USER_DEFAULT_ORDER_DIRECTION, + USER_DEFAULT_AVAILABLE_SEARCH, + USER_DEFAULT_AVAILABLE_ORDER_BY + ) + { _search, _limit, _offset, _order }: PaginationListDto, + @PaginationQueryFilterInBoolean('isActive', USER_DEFAULT_IS_ACTIVE) + isActive: Record, + @PaginationQueryFilterInBoolean('blocked', USER_DEFAULT_BLOCKED) + blocked: Record + ): Promise { + const find: Record = { + ..._search, + ...isActive, + ...blocked, + }; + + const users: IUserEntity[] = await this.userService.findAll(find, { + paging: { + limit: _limit, + offset: _offset, + }, + order: _order, + }); + const total: number = await this.userService.getTotal(find); + const totalPage: number = this.paginationService.totalPage( + total, + _limit + ); + + return { + _pagination: { total, totalPage }, + data: users, + }; + } + + @UserGetDoc() + @Response('user.get', { + serialization: UserGetSerialization, + }) + @UserGetGuard() + @RequestParamGuard(UserRequestDto) + @AuthPermissionProtected(ENUM_AUTH_PERMISSIONS.USER_READ) + @AuthJwtAdminAccessProtected() + @Get('get/:user') + async get(@GetUser() user: UserDoc): Promise { + const userWithRole: IUserDoc = await this.userService.joinWithRole( + user + ); + return { data: userWithRole.toObject() }; + } + + @UserCreateDoc() + @Response('user.create', { + serialization: ResponseIdSerialization, + }) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_CREATE + ) + @AuthJwtAdminAccessProtected() + @Post('/create') + async create( + @Body() + { username, email, mobileNumber, role, ...body }: UserCreateDto + ): Promise { + const checkRole = await this.roleService.exist(role); + if (!checkRole) { + throw new NotFoundException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR, + message: 'role.error.notFound', + }); + } + + const usernameExist: boolean = await this.userService.existByUsername( + username + ); + if (usernameExist) { + throw new ConflictException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_USERNAME_EXISTS_ERROR, + message: 'user.error.usernameExist', + }); + } + + const emailExist: boolean = await this.userService.existByEmail(email); + if (emailExist) { + throw new ConflictException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_EMAIL_EXIST_ERROR, + message: 'user.error.emailExist', + }); + } + + if (mobileNumber) { + const mobileNumberExist: boolean = + await this.userService.existByMobileNumber(mobileNumber); + if (mobileNumberExist) { + throw new ConflictException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_MOBILE_NUMBER_EXIST_ERROR, + message: 'user.error.mobileNumberExist', + }); + } + } + + try { + const password: IAuthPassword = + await this.authService.createPassword(body.password); + + const created: UserDoc = await this.userService.create( + { username, email, mobileNumber, role, ...body }, + password + ); + + return { + data: { _id: created._id }, + }; + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + } + + @UserDeleteDoc() + @Response('user.delete') + @UserDeleteGuard() + @RequestParamGuard(UserRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_DELETE + ) + @AuthJwtAdminAccessProtected() + @Delete('/delete/:user') + async delete(@GetUser() user: UserDoc): Promise { + try { + await this.userService.delete(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @UserUpdateDoc() + @Response('user.update', { + serialization: ResponseIdSerialization, + }) + @UserUpdateGuard() + @RequestParamGuard(UserRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_UPDATE + ) + @AuthJwtAdminAccessProtected() + @Put('/update/:user') + async update( + @GetUser() user: UserDoc, + @Body() + body: UserUpdateNameDto + ): Promise { + try { + await this.userService.updateName(user, body); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return { + data: { _id: user._id }, + }; + } + + @UserInactiveDoc() + @Response('user.inactive') + @UserUpdateInactiveGuard() + @RequestParamGuard(UserRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_UPDATE, + ENUM_AUTH_PERMISSIONS.USER_INACTIVE + ) + @AuthJwtAdminAccessProtected() + @Patch('/update/:user/inactive') + async inactive(@GetUser() user: UserDoc): Promise { + try { + await this.userService.inactive(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @UserActiveDoc() + @Response('user.active') + @UserUpdateActiveGuard() + @RequestParamGuard(UserRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_UPDATE, + ENUM_AUTH_PERMISSIONS.USER_ACTIVE + ) + @AuthJwtAdminAccessProtected() + @Patch('/update/:user/active') + async active(@GetUser() user: UserDoc): Promise { + try { + await this.userService.active(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @UserImportDoc() + @Response('user.import', { + serialization: UserImportSerialization, + }) + @UploadFileSingle('file') + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_CREATE, + ENUM_AUTH_PERMISSIONS.USER_IMPORT + ) + @AuthJwtAdminAccessProtected() + @Post('/import') + async import( + @UploadedFile( + FileRequiredPipe, + FileSizeExcelPipe, + FileTypeExcelPipe, + FileExtractPipe, + new FileValidationPipe(UserImportDto) + ) + file: IFileExtract + ): Promise { + return { data: { file } }; + } + + @UserExportDoc() + @ResponseExcel({ + serialization: UserListSerialization, + fileType: ENUM_HELPER_FILE_TYPE.CSV, + }) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_EXPORT + ) + @AuthJwtAdminAccessProtected() + @HttpCode(HttpStatus.OK) + @Post('/export') + async export(): Promise { + const users: IUserEntity[] = await this.userService.findAll({}); + + return { data: users }; + } + + @UserBlockedDoc() + @Response('user.blocked') + @UserUpdateBlockedGuard() + @RequestParamGuard(UserRequestDto) + @AuthPermissionProtected( + ENUM_AUTH_PERMISSIONS.USER_READ, + ENUM_AUTH_PERMISSIONS.USER_UPDATE, + ENUM_AUTH_PERMISSIONS.USER_BLOCKED + ) + @AuthJwtAdminAccessProtected() + @Patch('/update/:user/blocked') + async blocked(@GetUser() user: UserDoc): Promise { + try { + await this.userService.blocked(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } +} diff --git a/src/modules/user/controllers/user.controller.ts b/src/modules/user/controllers/user.controller.ts new file mode 100644 index 0000000..16df422 --- /dev/null +++ b/src/modules/user/controllers/user.controller.ts @@ -0,0 +1,546 @@ +import { + BadRequestException, + Body, + Controller, + ForbiddenException, + Get, + HttpCode, + HttpStatus, + InternalServerErrorException, + NotFoundException, + Patch, + Post, + UploadedFile, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { + AuthJwtAccessProtected, + AuthJwtPayload, + AuthJwtRefreshProtected, + AuthJwtToken, +} from 'src/common/auth/decorators/auth.jwt.decorator'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { AwsS3Service } from 'src/common/aws/services/aws.s3.service'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { UploadFileSingle } from 'src/common/file/decorators/file.decorator'; +import { IFile } from 'src/common/file/interfaces/file.interface'; +import { FileRequiredPipe } from 'src/common/file/pipes/file.required.pipe'; +import { FileSizeImagePipe } from 'src/common/file/pipes/file.size.pipe'; +import { FileTypeImagePipe } from 'src/common/file/pipes/file.type.pipe'; +import { ENUM_LOGGER_ACTION } from 'src/common/logger/constants/logger.enum.constant'; +import { Logger } from 'src/common/logger/decorators/logger.decorator'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { IResponse } from 'src/common/response/interfaces/response.interface'; +import { SettingService } from 'src/common/setting/services/setting.service'; +import { PermissionDoc, PermissionEntity } from "src/modules/permission/repository/entities/permission.entity"; +import { PermissionService } from 'src/modules/permission/services/permission.service'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { GetUser } from 'src/modules/user/decorators/user.decorator'; +import { UserProfileGuard } from 'src/modules/user/decorators/user.public.decorator'; +import { + UserChangePasswordDoc, + UserGrantPermissionDoc, + UserInfoDoc, + UserLoginDoc, + UserProfileDoc, + UserRefreshDoc, + UserUploadProfileDoc, +} from 'src/modules/user/docs/user.doc'; +import { UserChangePasswordDto } from 'src/modules/user/dtos/user.change-password.dto'; +import { UserGrantPermissionDto } from 'src/modules/user/dtos/user.grant-permission.dto'; +import { UserLoginDto } from 'src/modules/user/dtos/user.login.dto'; +import { + IUserDoc, + IUserEntity, +} from 'src/modules/user/interfaces/user.interface'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { UserGrantPermissionSerialization } from 'src/modules/user/serializations/user.grant-permission.serialization'; +import { UserInfoSerialization } from 'src/modules/user/serializations/user.info.serialization'; +import { UserLoginSerialization } from 'src/modules/user/serializations/user.login.serialization'; +import { UserPayloadPermissionSerialization } from 'src/modules/user/serializations/user.payload-permission.serialization'; +import { UserPayloadSerialization } from 'src/modules/user/serializations/user.payload.serialization'; +import { UserProfileSerialization } from 'src/modules/user/serializations/user.profile.serialization'; +import { UserService } from 'src/modules/user/services/user.service'; +import { IAuthPassword } from "../../../common/auth/interfaces/auth.interface"; +import { IPermissionGroup } from "../../permission/interfaces/permission.interface"; + +@ApiTags('modules.user') +@Controller({ + version: '1', + path: '/user', +}) +export class UserController { + constructor( + private readonly userService: UserService, + private readonly roleService: RoleService, + private readonly awsService: AwsS3Service, + private readonly authService: AuthService, + private readonly settingService: SettingService, + private readonly permissionService: PermissionService + ) {} + + @UserLoginDoc() + @Response('user.login', { + serialization: UserLoginSerialization, + }) + @Logger(ENUM_LOGGER_ACTION.LOGIN, { tags: ['login', 'withEmail'] }) + @HttpCode(HttpStatus.OK) + @Post('/login') + async login( + @Body() { username, password, rememberMe }: UserLoginDto + ): Promise { + const user: UserDoc = await this.userService.findOneByUsername( + username + ); + if (!user) { + throw new NotFoundException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR, + message: 'user.error.notFound', + }); + } + + const passwordAttempt: boolean = + await this.settingService.getPasswordAttempt(); + const maxPasswordAttempt: number = + await this.settingService.getMaxPasswordAttempt(); + if (passwordAttempt && user.passwordAttempt >= maxPasswordAttempt) { + throw new ForbiddenException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_ATTEMPT_MAX_ERROR, + message: 'user.error.passwordAttemptMax', + }); + } + + const validate: boolean = await this.authService.validateUser( + password, + user.password + ); + if (!validate) { + try { + await this.userService.increasePasswordAttempt(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + throw new BadRequestException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_NOT_MATCH_ERROR, + message: 'user.error.passwordNotMatch', + }); + } else if (user.blocked) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_BLOCKED_ERROR, + message: 'user.error.blocked', + }); + } else if (!user.isActive || user.inactivePermanent) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_INACTIVE_ERROR, + message: 'user.error.inactive', + }); + } + + const userWithRole: IUserDoc = await this.userService.joinWithRole( + user + ); + if (!userWithRole.role.isActive) { + throw new ForbiddenException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_INACTIVE_ERROR, + message: 'role.error.inactive', + }); + } + + try { + await this.userService.resetPasswordAttempt(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + const payload: UserPayloadSerialization = + await this.userService.payloadSerialization(userWithRole); + const tokenType: string = await this.authService.getTokenType(); + const expiresIn: number = + await this.authService.getAccessTokenExpirationTime(); + rememberMe = rememberMe ? true : false; + const payloadAccessToken: Record = + await this.authService.createPayloadAccessToken( + payload, + rememberMe + ); + const payloadRefreshToken: Record = + await this.authService.createPayloadRefreshToken( + payload._id, + rememberMe, + { + loginDate: payloadAccessToken.loginDate, + } + ); + + const payloadEncryption = await this.authService.getPayloadEncryption(); + let payloadHashedAccessToken: Record | string = + payloadAccessToken; + let payloadHashedRefreshToken: Record | string = + payloadRefreshToken; + + if (payloadEncryption) { + payloadHashedAccessToken = + await this.authService.encryptAccessToken(payloadAccessToken); + payloadHashedRefreshToken = + await this.authService.encryptRefreshToken(payloadRefreshToken); + } + + const accessToken: string = await this.authService.createAccessToken( + payloadHashedAccessToken + ); + + const refreshToken: string = await this.authService.createRefreshToken( + payloadHashedRefreshToken, + { rememberMe } + ); + + const checkPasswordExpired: boolean = + await this.authService.checkPasswordExpired(user.passwordExpired); + + if (checkPasswordExpired) { + return { + _metadata: { + // override status code and message + customProperty: { + // override status code and message + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_EXPIRED_ERROR, + message: 'user.error.passwordExpired', + }, + }, + data: { tokenType, expiresIn, accessToken, refreshToken }, + }; + } + + return { + data: { + tokenType, + expiresIn, + accessToken, + refreshToken, + }, + }; + } + + @UserRefreshDoc() + @Response('user.refresh', { serialization: UserLoginSerialization }) + @AuthJwtRefreshProtected() + @HttpCode(HttpStatus.OK) + @Post('/refresh') + async refresh( + @AuthJwtPayload() + { _id, rememberMe, loginDate }: Record, + @AuthJwtToken() refreshToken: string + ): Promise { + const user: UserDoc = await this.userService.findOneById(_id); + + if (!user) { + throw new NotFoundException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR, + message: 'user.error.notFound', + }); + } else if (user.blocked) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_BLOCKED_ERROR, + message: 'user.error.blocked', + }); + } else if (!user.isActive || user.inactivePermanent) { + throw new ForbiddenException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_INACTIVE_ERROR, + message: 'user.error.inactive', + }); + } + + const userWithRole: IUserDoc = await this.userService.joinWithRole( + user + ); + if (!userWithRole.role.isActive) { + throw new ForbiddenException({ + statusCode: ENUM_ROLE_STATUS_CODE_ERROR.ROLE_INACTIVE_ERROR, + message: 'role.error.inactive', + }); + } + + const checkPasswordExpired: boolean = + await this.authService.checkPasswordExpired(user.passwordExpired); + + if (checkPasswordExpired) { + throw new ForbiddenException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_EXPIRED_ERROR, + message: 'user.error.passwordExpired', + }); + } + + const payload: UserPayloadSerialization = + await this.userService.payloadSerialization(userWithRole); + const tokenType: string = await this.authService.getTokenType(); + const expiresIn: number = + await this.authService.getAccessTokenExpirationTime(); + const payloadAccessToken: Record = + await this.authService.createPayloadAccessToken( + payload, + rememberMe, + { + loginDate, + } + ); + + const payloadEncryption = await this.authService.getPayloadEncryption(); + let payloadHashedAccessToken: Record | string = + payloadAccessToken; + + if (payloadEncryption) { + payloadHashedAccessToken = + await this.authService.encryptAccessToken(payloadAccessToken); + } + + const accessToken: string = await this.authService.createAccessToken( + payloadHashedAccessToken + ); + + return { + data: { + tokenType, + expiresIn, + accessToken, + refreshToken, + }, + }; + } + + @UserChangePasswordDoc() + @Response('user.changePassword') + @AuthJwtAccessProtected() + @Patch('/change-password') + async changePassword( + @Body() body: UserChangePasswordDto, + @AuthJwtPayload('_id') _id: string + ): Promise { + const user: UserDoc = await this.userService.findOneById(_id); + if (!user) { + throw new NotFoundException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR, + message: 'user.error.notFound', + }); + } + + const passwordAttempt: boolean = + await this.settingService.getPasswordAttempt(); + const maxPasswordAttempt: number = + await this.settingService.getMaxPasswordAttempt(); + if (passwordAttempt && user.passwordAttempt >= maxPasswordAttempt) { + throw new ForbiddenException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_ATTEMPT_MAX_ERROR, + message: 'user.error.passwordAttemptMax', + }); + } + + const matchPassword: boolean = await this.authService.validateUser( + body.oldPassword, + user.password + ); + if (!matchPassword) { + try { + await this.userService.increasePasswordAttempt(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + throw new BadRequestException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_NOT_MATCH_ERROR, + message: 'user.error.passwordNotMatch', + }); + } + + const newMatchPassword: boolean = await this.authService.validateUser( + body.newPassword, + user.password + ); + if (newMatchPassword) { + throw new BadRequestException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_NEW_MUST_DIFFERENCE_ERROR, + message: 'user.error.newPasswordMustDifference', + }); + } + + try { + await this.userService.resetPasswordAttempt(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + try { + const password: IAuthPassword = + await this.authService.createPassword(body.newPassword); + + await this.userService.updatePassword(user, password); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } + + @UserInfoDoc() + @Response('user.info', { serialization: UserInfoSerialization }) + @AuthJwtAccessProtected() + @Get('/info') + async info( + @AuthJwtPayload() user: UserPayloadSerialization + ): Promise { + return { data: user }; + } + + @UserGrantPermissionDoc() + @Response('user.grantPermission', { + serialization: UserGrantPermissionSerialization, + }) + @AuthJwtAccessProtected() + @HttpCode(HttpStatus.OK) + @Post('/grant-permission') + async grantPermission( + @AuthJwtPayload() payload: UserPayloadSerialization, + @Body() { scope }: UserGrantPermissionDto + ): Promise { + const user: UserDoc = await this.userService.findOneById(payload._id); + if (!user) { + throw new NotFoundException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR, + message: 'user.error.notFound', + }); + } + + const userWithRole: IUserDoc = await this.userService.joinWithRole( + user + ); + const permissions: PermissionDoc[] = + await this.permissionService.findAllByIds( + userWithRole.role.permissions + ); + const grantPermissions: IPermissionGroup[] = + await this.permissionService.groupingByGroups(permissions, scope); + + + const payloadPermission: UserPayloadPermissionSerialization = + await this.userService.payloadPermissionSerialization( + user._id, + grantPermissions + ); + + const expiresIn: number = + await this.authService.getPermissionTokenExpirationTime(); + const payloadPermissionToken: Record = + await this.authService.createPayloadPermissionToken( + payloadPermission + ); + + + const payloadEncryption = await this.authService.getPayloadEncryption(); + let payloadHashedPermissionToken: Record | string = + payloadPermissionToken; + + if (payloadEncryption) { + payloadHashedPermissionToken = + await this.authService.encryptPermissionToken( + payloadPermissionToken + ); + } + + const permissionToken: string = + await this.authService.createPermissionToken( + payloadHashedPermissionToken + ); + + return { + data: { permissionToken, expiresIn }, + }; + } + + @UserProfileDoc() + @Response('user.profile', { + serialization: UserProfileSerialization, + }) + @UserProfileGuard() + @AuthJwtAccessProtected() + @Get('/profile') + async profile(@GetUser() user: UserDoc): Promise { + const userWithRole: IUserDoc = await this.userService.joinWithRole( + user + ); + return { data: userWithRole.toObject() }; + } + + @UserUploadProfileDoc() + @Response('user.upload') + @UserProfileGuard() + @AuthJwtAccessProtected() + @UploadFileSingle('file') + @HttpCode(HttpStatus.OK) + @Post('/profile/upload') + async upload( + @GetUser() usr: IUserDoc, + @UploadedFile(FileRequiredPipe, FileSizeImagePipe, FileTypeImagePipe) + file: IFile + ): Promise { + const user: UserDoc = await this.userService.findOneById(usr._id); + + const filename: string = file.originalname; + const content: Buffer = file.buffer; + const mime: string = filename + .substring(filename.lastIndexOf('.') + 1, filename.length) + .toUpperCase(); + + const path = await this.userService.createPhotoFilename(); + + try { + const aws: AwsS3Serialization = + await this.awsService.putItemInBucket( + `${path.filename}.${mime}`, + content, + { + path: `${path.path}/${user._id}`, + } + ); + await this.userService.updatePhoto(user, aws); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } +} diff --git a/src/modules/user/controllers/user.public.controller.ts b/src/modules/user/controllers/user.public.controller.ts new file mode 100644 index 0000000..b327687 --- /dev/null +++ b/src/modules/user/controllers/user.public.controller.ts @@ -0,0 +1,119 @@ +import { + Body, + ConflictException, + Controller, + Delete, + InternalServerErrorException, + Post, +} from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { + AuthJwtPayload, + AuthJwtPublicAccessProtected, +} from 'src/common/auth/decorators/auth.jwt.decorator'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant'; +import { Response } from 'src/common/response/decorators/response.decorator'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { + UserDeleteSelfDoc, + UserSignUpDoc, +} from 'src/modules/user/docs/user.public.doc'; +import { UserSignUpDto } from 'src/modules/user/dtos/user.sign-up.dto'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { UserService } from 'src/modules/user/services/user.service'; + +@ApiTags('modules.public.user') +@Controller({ + version: '1', + path: '/user', +}) +export class UserPublicController { + constructor( + private readonly userService: UserService, + private readonly authService: AuthService, + private readonly roleService: RoleService + ) {} + + @UserSignUpDoc() + @Response('user.signUp') + @Post('/sign-up') + async signUp( + @Body() + { email, mobileNumber, username, ...body }: UserSignUpDto + ): Promise { + const role: RoleDoc = await this.roleService.findOneByName('user'); + + const usernameExist: boolean = await this.userService.existByUsername( + username + ); + if (usernameExist) { + throw new ConflictException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_USERNAME_EXISTS_ERROR, + message: 'user.error.usernameExist', + }); + } + + const emailExist: boolean = await this.userService.existByEmail(email); + if (emailExist) { + throw new ConflictException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_EMAIL_EXIST_ERROR, + message: 'user.error.emailExist', + }); + } + + if (mobileNumber) { + const mobileNumberExist: boolean = + await this.userService.existByMobileNumber(mobileNumber); + if (mobileNumberExist) { + throw new ConflictException({ + statusCode: + ENUM_USER_STATUS_CODE_ERROR.USER_MOBILE_NUMBER_EXIST_ERROR, + message: 'user.error.mobileNumberExist', + }); + } + } + + try { + const password = await this.authService.createPassword( + body.password + ); + + await this.userService.create( + { email, mobileNumber, username, ...body, role: role._id }, + password + ); + + return; + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + } + + @UserDeleteSelfDoc() + @Response('user.deleteSelf') + @AuthJwtPublicAccessProtected() + @Delete('/delete') + async deleteSelf(@AuthJwtPayload('_id') _id: string): Promise { + try { + const user: UserDoc = await this.userService.findOneById(_id); + + await this.userService.inactive(user); + } catch (err: any) { + throw new InternalServerErrorException({ + statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN, + message: 'http.serverError.internalServerError', + _error: err.message, + }); + } + + return; + } +} diff --git a/src/modules/user/decorators/user.admin.decorator.ts b/src/modules/user/decorators/user.admin.decorator.ts new file mode 100644 index 0000000..7aa8731 --- /dev/null +++ b/src/modules/user/decorators/user.admin.decorator.ts @@ -0,0 +1,42 @@ +import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'; +import { + USER_ACTIVE_META_KEY, + USER_BLOCKED_META_KEY, +} from 'src/modules/user/constants/user.constant'; +import { UserActiveGuard } from 'src/modules/user/guards/user.active.guard'; +import { UserBlockedGuard } from 'src/modules/user/guards/user.blocked.guard'; +import { UserNotFoundGuard } from 'src/modules/user/guards/user.not-found.guard'; +import { UserPutToRequestGuard } from 'src/modules/user/guards/user.put-to-request.guard'; + +export function UserGetGuard(): MethodDecorator { + return applyDecorators(UseGuards(UserPutToRequestGuard, UserNotFoundGuard)); +} + +export function UserDeleteGuard(): MethodDecorator { + return applyDecorators(UseGuards(UserPutToRequestGuard, UserNotFoundGuard)); +} + +export function UserUpdateGuard(): MethodDecorator { + return applyDecorators(UseGuards(UserPutToRequestGuard, UserNotFoundGuard)); +} + +export function UserUpdateInactiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards(UserPutToRequestGuard, UserNotFoundGuard, UserActiveGuard), + SetMetadata(USER_ACTIVE_META_KEY, [true]) + ); +} + +export function UserUpdateActiveGuard(): MethodDecorator { + return applyDecorators( + UseGuards(UserPutToRequestGuard, UserNotFoundGuard, UserActiveGuard), + SetMetadata(USER_ACTIVE_META_KEY, [false]) + ); +} + +export function UserUpdateBlockedGuard(): MethodDecorator { + return applyDecorators( + UseGuards(UserPutToRequestGuard, UserNotFoundGuard, UserBlockedGuard), + SetMetadata(USER_BLOCKED_META_KEY, [false]) + ); +} diff --git a/src/modules/user/decorators/user.decorator.ts b/src/modules/user/decorators/user.decorator.ts new file mode 100644 index 0000000..7ae9e99 --- /dev/null +++ b/src/modules/user/decorators/user.decorator.ts @@ -0,0 +1,12 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; + +export const GetUser = createParamDecorator( + (returnPlain: boolean, ctx: ExecutionContext): UserDoc | UserEntity => { + const { __user } = ctx.switchToHttp().getRequest(); + return returnPlain ? __user.toObject() : __user; + } +); diff --git a/src/modules/user/decorators/user.public.decorator.ts b/src/modules/user/decorators/user.public.decorator.ts new file mode 100644 index 0000000..b8608de --- /dev/null +++ b/src/modules/user/decorators/user.public.decorator.ts @@ -0,0 +1,9 @@ +import { applyDecorators, UseGuards } from '@nestjs/common'; +import { UserPayloadPutToRequestGuard } from 'src/modules/user/guards/payload/user.payload.put-to-request.guard'; +import { UserNotFoundGuard } from 'src/modules/user/guards/user.not-found.guard'; + +export function UserProfileGuard(): MethodDecorator { + return applyDecorators( + UseGuards(UserPayloadPutToRequestGuard, UserNotFoundGuard) + ); +} diff --git a/src/modules/user/docs/user.admin.doc.ts b/src/modules/user/docs/user.admin.doc.ts new file mode 100644 index 0000000..b1f0d7b --- /dev/null +++ b/src/modules/user/docs/user.admin.doc.ts @@ -0,0 +1,158 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { Doc, DocPaging } from 'src/common/doc/decorators/doc.decorator'; +import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization'; +import { + UserDocParamsGet, + UserDocQueryBlocked, + UserDocQueryIsActive, +} from 'src/modules/user/constants/user.doc.constant'; +import { UserGetSerialization } from 'src/modules/user/serializations/user.get.serialization'; +import { UserImportSerialization } from 'src/modules/user/serializations/user.import.serialization'; +import { UserListSerialization } from 'src/modules/user/serializations/user.list.serialization'; + +export function UserListDoc(): MethodDecorator { + return applyDecorators( + DocPaging('user.list', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + queries: [...UserDocQueryIsActive, ...UserDocQueryBlocked], + }, + response: { + serialization: UserListSerialization, + }, + }) + ); +} + +export function UserGetDoc(): MethodDecorator { + return applyDecorators( + Doc('user.get', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: UserDocParamsGet, + }, + response: { serialization: UserGetSerialization }, + }) + ); +} + +export function UserCreateDoc(): MethodDecorator { + return applyDecorators( + Doc('user.create', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + response: { + httpStatus: HttpStatus.CREATED, + serialization: ResponseIdSerialization, + }, + }) + ); +} + +export function UserUpdateDoc(): MethodDecorator { + return applyDecorators( + Doc('user.update', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: UserDocParamsGet, + }, + response: { serialization: ResponseIdSerialization }, + }) + ); +} + +export function UserDeleteDoc(): MethodDecorator { + return applyDecorators( + Doc('user.delete', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: UserDocParamsGet, + }, + }) + ); +} + +export function UserImportDoc(): MethodDecorator { + return applyDecorators( + Doc('user.import', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + response: { + httpStatus: HttpStatus.CREATED, + serialization: UserImportSerialization, + }, + }) + ); +} + +export function UserExportDoc(): MethodDecorator { + return applyDecorators( + Doc('user.export', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + response: { + httpStatus: HttpStatus.OK, + }, + }) + ); +} + +export function UserActiveDoc(): MethodDecorator { + return applyDecorators( + Doc('user.active', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: UserDocParamsGet, + }, + }) + ); +} + +export function UserInactiveDoc(): MethodDecorator { + return applyDecorators( + Doc('user.inactive', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: UserDocParamsGet, + }, + }) + ); +} + +export function UserBlockedDoc(): MethodDecorator { + return applyDecorators( + Doc('user.blocked', { + auth: { + jwtAccessToken: true, + permissionToken: true, + }, + request: { + params: UserDocParamsGet, + }, + }) + ); +} diff --git a/src/modules/user/docs/user.doc.ts b/src/modules/user/docs/user.doc.ts new file mode 100644 index 0000000..c10576d --- /dev/null +++ b/src/modules/user/docs/user.doc.ts @@ -0,0 +1,98 @@ +import { applyDecorators } from '@nestjs/common'; +import { ENUM_DOC_REQUEST_BODY_TYPE } from 'src/common/doc/constants/doc.enum.constant'; +import { Doc } from 'src/common/doc/decorators/doc.decorator'; +import { UserGrantPermissionSerialization } from 'src/modules/user/serializations/user.grant-permission.serialization'; +import { UserLoginSerialization } from 'src/modules/user/serializations/user.login.serialization'; +import { UserPayloadSerialization } from 'src/modules/user/serializations/user.payload.serialization'; +import { UserProfileSerialization } from 'src/modules/user/serializations/user.profile.serialization'; + +export function UserProfileDoc(): MethodDecorator { + return applyDecorators( + Doc('user.profile', { + auth: { + jwtAccessToken: true, + }, + response: { + serialization: UserProfileSerialization, + }, + }) + ); +} + +export function UserUploadProfileDoc(): MethodDecorator { + return applyDecorators( + Doc('user.upload', { + auth: { + jwtAccessToken: true, + }, + request: { + bodyType: ENUM_DOC_REQUEST_BODY_TYPE.FORM_DATA, + file: { + multiple: false, + }, + }, + }) + ); +} + +export function UserLoginDoc(): MethodDecorator { + return applyDecorators( + Doc('user.login', { + auth: { + jwtAccessToken: true, + }, + response: { + serialization: UserLoginSerialization, + }, + }) + ); +} + +export function UserRefreshDoc(): MethodDecorator { + return applyDecorators( + Doc('user.refresh', { + auth: { + jwtRefreshToken: true, + }, + response: { + serialization: UserLoginSerialization, + }, + }) + ); +} + +export function UserInfoDoc(): MethodDecorator { + return applyDecorators( + Doc('user.info', { + auth: { + jwtAccessToken: true, + }, + response: { + serialization: UserPayloadSerialization, + }, + }) + ); +} + +export function UserChangePasswordDoc(): MethodDecorator { + return applyDecorators( + Doc('user.changePassword', { + auth: { + jwtAccessToken: true, + }, + }) + ); +} + +export function UserGrantPermissionDoc(): MethodDecorator { + return applyDecorators( + Doc('user.grantPermission', { + response: { + serialization: UserGrantPermissionSerialization, + }, + auth: { + jwtAccessToken: true, + }, + }) + ); +} diff --git a/src/modules/user/docs/user.public.doc.ts b/src/modules/user/docs/user.public.doc.ts new file mode 100644 index 0000000..64b0adb --- /dev/null +++ b/src/modules/user/docs/user.public.doc.ts @@ -0,0 +1,25 @@ +import { applyDecorators, HttpStatus } from '@nestjs/common'; +import { Doc } from 'src/common/doc/decorators/doc.decorator'; + +export function UserSignUpDoc(): MethodDecorator { + return applyDecorators( + Doc('user.signUp', { + auth: { + jwtAccessToken: false, + }, + response: { + httpStatus: HttpStatus.CREATED, + }, + }) + ); +} + +export function UserDeleteSelfDoc(): MethodDecorator { + return applyDecorators( + Doc('user.deleteSelf', { + auth: { + jwtAccessToken: true, + }, + }) + ); +} diff --git a/src/modules/user/dtos/user.active.dto.ts b/src/modules/user/dtos/user.active.dto.ts new file mode 100644 index 0000000..26db248 --- /dev/null +++ b/src/modules/user/dtos/user.active.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsBoolean, IsDate, IsNotEmpty } from 'class-validator'; + +export class UserActiveDto { + @ApiProperty({ + name: 'isActive', + required: true, + nullable: false, + }) + @IsBoolean() + @IsNotEmpty() + isActive: boolean; + + @ApiProperty({ + name: 'inactiveDate', + description: 'inactive date of user', + required: true, + nullable: false, + }) + @IsDate() + @IsNotEmpty() + @Type(() => Date) + inactiveDate: Date; +} diff --git a/src/modules/user/dtos/user.block.dto.ts b/src/modules/user/dtos/user.block.dto.ts new file mode 100644 index 0000000..24c2703 --- /dev/null +++ b/src/modules/user/dtos/user.block.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsBoolean, IsDate, IsNotEmpty } from 'class-validator'; + +export class UserBlockedDto { + @ApiProperty({ + name: 'blocked', + required: true, + nullable: false, + }) + @IsBoolean() + @IsNotEmpty() + blocked: boolean; + + @ApiProperty({ + name: 'blockedDate', + required: true, + nullable: false, + }) + @IsDate() + @IsNotEmpty() + @Type(() => Date) + blockedDate: Date; +} diff --git a/src/modules/user/dtos/user.change-password.dto.ts b/src/modules/user/dtos/user.change-password.dto.ts new file mode 100644 index 0000000..069ebbc --- /dev/null +++ b/src/modules/user/dtos/user.change-password.dto.ts @@ -0,0 +1,29 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty } from 'class-validator'; +import { IsPasswordStrong } from 'src/common/request/validations/request.is-password-strong.validation'; + +export class UserChangePasswordDto { + @ApiProperty({ + description: + "new string password, newPassword can't same with oldPassword", + example: `${faker.random.alphaNumeric(5).toLowerCase()}${faker.random + .alphaNumeric(5) + .toUpperCase()}@@!123`, + required: true, + }) + @IsPasswordStrong() + @IsNotEmpty() + readonly newPassword: string; + + @ApiProperty({ + description: 'old string password', + example: `${faker.random.alphaNumeric(5).toLowerCase()}${faker.random + .alphaNumeric(5) + .toUpperCase()}@@!123`, + required: true, + }) + @IsString() + @IsNotEmpty() + readonly oldPassword: string; +} diff --git a/src/modules/user/dtos/user.create.dto.ts b/src/modules/user/dtos/user.create.dto.ts new file mode 100644 index 0000000..7a927b7 --- /dev/null +++ b/src/modules/user/dtos/user.create.dto.ts @@ -0,0 +1,91 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsString, + IsNotEmpty, + IsEmail, + MaxLength, + MinLength, + IsUUID, + IsOptional, + ValidateIf, +} from 'class-validator'; +import { IsPasswordStrong } from 'src/common/request/validations/request.is-password-strong.validation'; +import { MobileNumberAllowed } from 'src/common/request/validations/request.mobile-number-allowed.validation'; + +export class UserCreateDto { + @ApiProperty({ + example: faker.internet.userName(), + required: true, + }) + @IsString() + @IsNotEmpty() + @MaxLength(100) + @Type(() => String) + readonly username: string; + + @ApiProperty({ + example: faker.internet.email(), + required: true, + }) + @IsEmail() + @IsNotEmpty() + @MaxLength(100) + @Type(() => String) + readonly email: string; + + @ApiProperty({ + example: faker.name.firstName(), + required: true, + }) + @IsString() + @IsNotEmpty() + @MinLength(1) + @MaxLength(30) + @Type(() => String) + readonly firstName: string; + + @ApiProperty({ + example: faker.name.lastName(), + required: true, + }) + @IsString() + @IsNotEmpty() + @MinLength(1) + @MaxLength(30) + @Type(() => String) + readonly lastName: string; + + @ApiProperty({ + example: faker.phone.number('62812#########'), + required: true, + }) + @IsString() + @IsOptional() + @MinLength(10) + @MaxLength(14) + @ValidateIf((e) => e.mobileNumber !== '') + @Type(() => String) + @MobileNumberAllowed() + readonly mobileNumber?: string; + + @ApiProperty({ + example: faker.datatype.uuid(), + required: true, + }) + @IsNotEmpty() + @IsUUID('4') + readonly role: string; + + @ApiProperty({ + description: 'string password', + example: `${faker.random.alphaNumeric(5).toLowerCase()}${faker.random + .alphaNumeric(5) + .toUpperCase()}@@!123`, + required: true, + }) + @IsNotEmpty() + @IsPasswordStrong() + readonly password: string; +} diff --git a/src/modules/user/dtos/user.grant-permission.dto.ts b/src/modules/user/dtos/user.grant-permission.dto.ts new file mode 100644 index 0000000..563fb0a --- /dev/null +++ b/src/modules/user/dtos/user.grant-permission.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsEnum, IsArray, ArrayNotEmpty } from 'class-validator'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; + +export class UserGrantPermissionDto { + @ApiProperty({ + description: 'scope for grant permission', + example: Object.values(ENUM_PERMISSION_GROUP), + required: true, + isArray: true, + }) + @IsEnum(ENUM_PERMISSION_GROUP, { each: true }) + @IsNotEmpty() + @IsArray() + @ArrayNotEmpty() + readonly scope: ENUM_PERMISSION_GROUP[]; +} diff --git a/src/modules/user/dtos/user.import.dto.ts b/src/modules/user/dtos/user.import.dto.ts new file mode 100644 index 0000000..5fe2ef6 --- /dev/null +++ b/src/modules/user/dtos/user.import.dto.ts @@ -0,0 +1,7 @@ +import { OmitType } from '@nestjs/swagger'; +import { UserCreateDto } from './user.create.dto'; + +export class UserImportDto extends OmitType(UserCreateDto, [ + 'role', + 'password', +] as const) {} diff --git a/src/modules/user/dtos/user.login.dto.ts b/src/modules/user/dtos/user.login.dto.ts new file mode 100644 index 0000000..6f590e8 --- /dev/null +++ b/src/modules/user/dtos/user.login.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { IsBoolean, IsOptional, ValidateIf } from 'class-validator'; +import { UserCreateDto } from 'src/modules/user/dtos/user.create.dto'; + +export class UserLoginDto extends PickType(UserCreateDto, [ + 'username', + 'password', +] as const) { + @ApiProperty({ + description: + 'if true refresh token expired will extend to 30d, else 7d', + example: false, + required: false, + }) + @IsOptional() + @IsBoolean() + @ValidateIf((e) => e.rememberMe !== '') + readonly rememberMe?: boolean; +} diff --git a/src/modules/user/dtos/user.password-attempt.dto.ts b/src/modules/user/dtos/user.password-attempt.dto.ts new file mode 100644 index 0000000..e578d1c --- /dev/null +++ b/src/modules/user/dtos/user.password-attempt.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber } from 'class-validator'; + +export class UserPasswordAttemptDto { + @ApiProperty({ + name: 'passwordAttempt', + description: 'password attempt of user', + required: true, + nullable: false, + }) + @IsNumber() + @IsNotEmpty() + passwordAttempt: number; +} diff --git a/src/modules/user/dtos/user.password-expired.dto.ts b/src/modules/user/dtos/user.password-expired.dto.ts new file mode 100644 index 0000000..239aa5e --- /dev/null +++ b/src/modules/user/dtos/user.password-expired.dto.ts @@ -0,0 +1,6 @@ +import { PickType } from '@nestjs/swagger'; +import { UserPasswordDto } from 'src/modules/user/dtos/user.password.dto'; + +export class UserPasswordExpiredDto extends PickType(UserPasswordDto, [ + 'passwordExpired', +] as const) {} diff --git a/src/modules/user/dtos/user.password.dto.ts b/src/modules/user/dtos/user.password.dto.ts new file mode 100644 index 0000000..d61ed65 --- /dev/null +++ b/src/modules/user/dtos/user.password.dto.ts @@ -0,0 +1,46 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsDate, IsNotEmpty, IsString } from 'class-validator'; + +export class UserPasswordDto { + @ApiProperty({ + name: 'password', + description: 'password hash of string password', + required: true, + nullable: false, + }) + @IsString() + @IsNotEmpty() + password: string; + + @ApiProperty({ + name: 'passwordExpired', + description: 'password expired date', + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsDate() + @Type(() => Date) + passwordExpired: Date; + + @ApiProperty({ + name: 'passwordCreated', + description: 'password created date', + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsDate() + @Type(() => Date) + passwordCreated: Date; + + @ApiProperty({ + name: 'salt', + required: true, + nullable: false, + }) + @IsString() + @IsNotEmpty() + salt: string; +} diff --git a/src/modules/user/dtos/user.photo.dto.ts b/src/modules/user/dtos/user.photo.dto.ts new file mode 100644 index 0000000..f7755ff --- /dev/null +++ b/src/modules/user/dtos/user.photo.dto.ts @@ -0,0 +1,9 @@ +import { Type } from 'class-transformer'; +import { ValidateNested } from 'class-validator'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; + +export class UserPhotoDto { + @ValidateNested() + @Type(() => AwsS3Serialization) + photo: AwsS3Serialization; +} diff --git a/src/modules/user/dtos/user.request.dto.ts b/src/modules/user/dtos/user.request.dto.ts new file mode 100644 index 0000000..83095ae --- /dev/null +++ b/src/modules/user/dtos/user.request.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class UserRequestDto { + @ApiProperty({ + name: 'user', + description: 'user id', + required: true, + nullable: false, + }) + @IsNotEmpty() + @IsUUID('4') + @Type(() => String) + user: string; +} diff --git a/src/modules/user/dtos/user.sign-up.dto.ts b/src/modules/user/dtos/user.sign-up.dto.ts new file mode 100644 index 0000000..8d05a7f --- /dev/null +++ b/src/modules/user/dtos/user.sign-up.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from '@nestjs/swagger'; +import { UserCreateDto } from './user.create.dto'; + +export class UserSignUpDto extends OmitType(UserCreateDto, ['role'] as const) {} diff --git a/src/modules/user/dtos/user.update-name.dto.ts b/src/modules/user/dtos/user.update-name.dto.ts new file mode 100644 index 0000000..0c6e8c6 --- /dev/null +++ b/src/modules/user/dtos/user.update-name.dto.ts @@ -0,0 +1,7 @@ +import { PickType } from '@nestjs/swagger'; +import { UserCreateDto } from './user.create.dto'; + +export class UserUpdateNameDto extends PickType(UserCreateDto, [ + 'firstName', + 'lastName', +] as const) {} diff --git a/src/modules/user/guards/payload/user.payload.put-to-request.guard.ts b/src/modules/user/guards/payload/user.payload.put-to-request.guard.ts new file mode 100644 index 0000000..db024b2 --- /dev/null +++ b/src/modules/user/guards/payload/user.payload.put-to-request.guard.ts @@ -0,0 +1,20 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { UserService } from 'src/modules/user/services/user.service'; + +@Injectable() +export class UserPayloadPutToRequestGuard implements CanActivate { + constructor(private readonly userService: UserService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { user } = request; + + const check: UserDoc = await this.userService.findOneById(user._id, { + join: true, + }); + request.__user = check; + + return true; + } +} diff --git a/src/modules/user/guards/user.active.guard.ts b/src/modules/user/guards/user.active.guard.ts new file mode 100644 index 0000000..21d5a09 --- /dev/null +++ b/src/modules/user/guards/user.active.guard.ts @@ -0,0 +1,35 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { USER_ACTIVE_META_KEY } from 'src/modules/user/constants/user.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; + +@Injectable() +export class UserActiveGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const required: boolean[] = this.reflector.getAllAndOverride( + USER_ACTIVE_META_KEY, + [context.getHandler(), context.getClass()] + ); + + if (!required) { + return true; + } + + const { __user } = context.switchToHttp().getRequest(); + + if (!required.includes(__user.isActive)) { + throw new BadRequestException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_IS_ACTIVE_ERROR, + message: 'user.error.isActiveInvalid', + }); + } + return true; + } +} diff --git a/src/modules/user/guards/user.blocked.guard.ts b/src/modules/user/guards/user.blocked.guard.ts new file mode 100644 index 0000000..9115d31 --- /dev/null +++ b/src/modules/user/guards/user.blocked.guard.ts @@ -0,0 +1,35 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + BadRequestException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { USER_BLOCKED_META_KEY } from 'src/modules/user/constants/user.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; + +@Injectable() +export class UserBlockedGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + async canActivate(context: ExecutionContext): Promise { + const required: boolean[] = this.reflector.getAllAndOverride( + USER_BLOCKED_META_KEY, + [context.getHandler(), context.getClass()] + ); + + if (!required) { + return true; + } + + const { __user } = context.switchToHttp().getRequest(); + + if (!required.includes(__user.blocked)) { + throw new BadRequestException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_BLOCKED_ERROR, + message: 'user.error.blocked', + }); + } + return true; + } +} diff --git a/src/modules/user/guards/user.not-found.guard.ts b/src/modules/user/guards/user.not-found.guard.ts new file mode 100644 index 0000000..e8bc208 --- /dev/null +++ b/src/modules/user/guards/user.not-found.guard.ts @@ -0,0 +1,23 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + NotFoundException, +} from '@nestjs/common'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; + +@Injectable() +export class UserNotFoundGuard implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + const { __user } = context.switchToHttp().getRequest(); + + if (!__user) { + throw new NotFoundException({ + statusCode: ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR, + message: 'user.error.notFound', + }); + } + + return true; + } +} diff --git a/src/modules/user/guards/user.put-to-request.guard.ts b/src/modules/user/guards/user.put-to-request.guard.ts new file mode 100644 index 0000000..98c998d --- /dev/null +++ b/src/modules/user/guards/user.put-to-request.guard.ts @@ -0,0 +1,21 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { UserService } from 'src/modules/user/services/user.service'; + +@Injectable() +export class UserPutToRequestGuard implements CanActivate { + constructor(private readonly userService: UserService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const { params } = request; + const { user } = params; + + const check: UserDoc = await this.userService.findOneById(user, { + join: true, + }); + request.__user = check; + + return true; + } +} diff --git a/src/modules/user/interfaces/user.interface.ts b/src/modules/user/interfaces/user.interface.ts new file mode 100644 index 0000000..3a86765 --- /dev/null +++ b/src/modules/user/interfaces/user.interface.ts @@ -0,0 +1,16 @@ +import { + RoleDoc, + RoleEntity, +} from 'src/modules/role/repository/entities/role.entity'; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; + +export interface IUserEntity extends Omit { + role: RoleEntity; +} + +export interface IUserDoc extends Omit { + role: RoleDoc; +} diff --git a/src/modules/user/interfaces/user.service.interface.ts b/src/modules/user/interfaces/user.service.interface.ts new file mode 100644 index 0000000..14a3217 --- /dev/null +++ b/src/modules/user/interfaces/user.service.interface.ts @@ -0,0 +1,125 @@ +import { IAuthPassword } from 'src/common/auth/interfaces/auth.interface'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { + IDatabaseCreateOptions, + IDatabaseExistOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { UserCreateDto } from 'src/modules/user/dtos/user.create.dto'; +import { UserUpdateNameDto } from 'src/modules/user/dtos/user.update-name.dto'; +import { IUserDoc, IUserEntity } from "src/modules/user/interfaces/user.interface"; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; +import { UserPayloadPermissionSerialization } from 'src/modules/user/serializations/user.payload-permission.serialization'; +import { UserPayloadSerialization } from 'src/modules/user/serializations/user.payload.serialization'; +import { IPermissionGroup } from "../../permission/interfaces/permission.interface"; + +export interface IUserService { + findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise; + + findOneById(_id: string, options?: IDatabaseFindOneOptions): Promise; + + findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise; + + findOneByUsername( + username: string, + options?: IDatabaseFindOneOptions + ): Promise; + + getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise; + + create( + { + username, + firstName, + lastName, + email, + mobileNumber, + role, + }: UserCreateDto, + { passwordExpired, passwordHash, salt, passwordCreated }: IAuthPassword, + options?: IDatabaseCreateOptions + ): Promise; + + existByEmail( + email: string, + options?: IDatabaseExistOptions + ): Promise; + + joinWithRole(repository: UserDoc): Promise; + + existByMobileNumber( + mobileNumber: string, + options?: IDatabaseExistOptions + ): Promise; + + existByUsername( + username: string, + options?: IDatabaseExistOptions + ): Promise; + + delete(repository: UserDoc): Promise; + + updateName( + repository: UserDoc, + { firstName, lastName }: UserUpdateNameDto + ): Promise; + + updatePhoto( + repository: UserDoc, + photo: AwsS3Serialization + ): Promise; + + updatePassword( + repository: UserDoc, + { passwordHash, passwordExpired, salt, passwordCreated }: IAuthPassword + ): Promise; + + active(repository: UserDoc): Promise; + + inactive(repository: UserDoc): Promise; + + blocked(repository: UserDoc): Promise; + + unblocked(repository: UserDoc): Promise; + + maxPasswordAttempt(repository: UserDoc): Promise; + + increasePasswordAttempt(repository: UserDoc): Promise; + + resetPasswordAttempt(repository: UserDoc): Promise; + + updatePasswordExpired( + repository: UserDoc, + passwordExpired: Date + ): Promise; + + createPhotoFilename(): Promise>; + + payloadSerialization(data: IUserDoc): Promise; + + payloadPermissionSerialization( + _id: string, + permissions: IPermissionGroup[] + ): Promise; + + deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise; +} diff --git a/src/modules/user/repository/entities/user.entity.ts b/src/modules/user/repository/entities/user.entity.ts new file mode 100644 index 0000000..e81d1a8 --- /dev/null +++ b/src/modules/user/repository/entities/user.entity.ts @@ -0,0 +1,168 @@ +import { Prop, SchemaFactory } from '@nestjs/mongoose'; +import { CallbackWithoutResultAndOptionalError, Document } from 'mongoose'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract'; +import { DatabaseEntity } from 'src/common/database/decorators/database.decorator'; +import { RoleEntity } from 'src/modules/role/repository/entities/role.entity'; + +export const UserDatabaseName = 'users'; + +@DatabaseEntity({ collection: UserDatabaseName }) +export class UserEntity extends DatabaseMongoUUIDEntityAbstract { + @Prop({ + required: true, + index: true, + trim: true, + unique: true, + type: String, + maxlength: 100, + }) + username: string; + + @Prop({ + required: true, + index: true, + lowercase: true, + trim: true, + type: String, + maxlength: 50, + }) + firstName: string; + + @Prop({ + required: true, + index: true, + lowercase: true, + trim: true, + type: String, + maxlength: 50, + }) + lastName: string; + + @Prop({ + required: false, + sparse: true, + trim: true, + unique: true, + type: String, + maxlength: 15, + }) + mobileNumber?: string; + + @Prop({ + required: true, + index: true, + unique: true, + trim: true, + lowercase: true, + type: String, + maxlength: 100, + }) + email: string; + + @Prop({ + required: true, + ref: RoleEntity.name, + index: true, + }) + role: string; + + @Prop({ + required: true, + type: String, + }) + password: string; + + @Prop({ + required: true, + type: Date, + }) + passwordExpired: Date; + + @Prop({ + required: true, + type: Date, + }) + passwordCreated: Date; + + @Prop({ + required: true, + default: 0, + type: Number, + }) + passwordAttempt: number; + + @Prop({ + required: true, + type: Date, + }) + signUpDate: Date; + + @Prop({ + required: true, + type: String, + }) + salt: string; + + @Prop({ + required: true, + default: true, + index: true, + type: Boolean, + }) + isActive: boolean; + + @Prop({ + required: true, + default: false, + index: true, + type: Boolean, + }) + inactivePermanent: boolean; + + @Prop({ + required: false, + type: Date, + }) + inactiveDate?: Date; + + @Prop({ + required: true, + default: false, + index: true, + type: Boolean, + }) + blocked: boolean; + + @Prop({ + required: false, + type: Date, + }) + blockedDate?: Date; + + @Prop({ + required: false, + _id: false, + type: { + path: String, + pathWithFilename: String, + filename: String, + completedUrl: String, + baseUrl: String, + mime: String, + }, + }) + photo?: AwsS3Serialization; +} + +export const UserSchema = SchemaFactory.createForClass(UserEntity); + +export type UserDoc = UserEntity & Document; + +UserSchema.pre('save', function (next: CallbackWithoutResultAndOptionalError) { + this.email = this.email.toLowerCase(); + this.firstName = this.firstName.toLowerCase(); + this.lastName = this.lastName.toLowerCase(); + + next(); +}); diff --git a/src/modules/user/repository/repositories/user.repository.ts b/src/modules/user/repository/repositories/user.repository.ts new file mode 100644 index 0000000..a28fd3d --- /dev/null +++ b/src/modules/user/repository/repositories/user.repository.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { Model } from 'mongoose'; +import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract'; +import { DatabaseModel } from 'src/common/database/decorators/database.decorator'; +import { RoleEntity } from 'src/modules/role/repository/entities/role.entity'; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; + +@Injectable() +export class UserRepository extends DatabaseMongoUUIDRepositoryAbstract< + UserEntity, + UserDoc +> { + constructor( + @DatabaseModel(UserEntity.name) + private readonly userModel: Model + ) { + super(userModel, { + path: 'role', + localField: 'role', + foreignField: '_id', + model: RoleEntity.name, + }); + } +} diff --git a/src/modules/user/repository/user.repository.module.ts b/src/modules/user/repository/user.repository.module.ts new file mode 100644 index 0000000..739abcc --- /dev/null +++ b/src/modules/user/repository/user.repository.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { + UserEntity, + UserSchema, +} from 'src/modules/user/repository/entities/user.entity'; +import { UserRepository } from 'src/modules/user/repository/repositories/user.repository'; + +@Module({ + providers: [UserRepository], + exports: [UserRepository], + controllers: [], + imports: [ + MongooseModule.forFeature( + [ + { + name: UserEntity.name, + schema: UserSchema, + }, + ], + DATABASE_CONNECTION_NAME + ), + ], +}) +export class UserRepositoryModule {} diff --git a/src/modules/user/serializations/user.get.serialization.ts b/src/modules/user/serializations/user.get.serialization.ts new file mode 100644 index 0000000..60b8b3d --- /dev/null +++ b/src/modules/user/serializations/user.get.serialization.ts @@ -0,0 +1,119 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { Exclude, Type } from 'class-transformer'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { RoleGetSerialization } from 'src/modules/role/serializations/role.get.serialization'; + +export class UserGetSerialization { + @ApiProperty({ example: faker.datatype.uuid() }) + @Type(() => String) + readonly _id: string; + + @ApiProperty({ + type: () => RoleGetSerialization, + }) + @Type(() => RoleGetSerialization) + readonly role: RoleGetSerialization; + + @ApiProperty({ + example: faker.internet.userName(), + }) + readonly username: string; + + @ApiProperty({ + example: faker.internet.email(), + }) + readonly email: string; + + @ApiProperty({ + example: faker.internet.email(), + }) + readonly mobileNumber?: string; + + @ApiProperty({ + example: true, + }) + readonly isActive: boolean; + + @ApiProperty({ + example: true, + }) + readonly inactivePermanent: boolean; + + @ApiProperty({ + required: false, + nullable: true, + example: faker.date.recent(), + }) + readonly inactiveDate?: Date; + + @ApiProperty({ + example: false, + }) + readonly blocked: boolean; + + @ApiProperty({ + required: false, + nullable: true, + example: faker.date.recent(), + }) + readonly blockedDate?: Date; + + @ApiProperty({ + example: faker.name.firstName(), + }) + readonly firstName: string; + + @ApiProperty({ + example: faker.name.lastName(), + }) + readonly lastName: string; + + @ApiProperty({ + allOf: [{ $ref: getSchemaPath(AwsS3Serialization) }], + }) + readonly photo?: AwsS3Serialization; + + @Exclude() + readonly password: string; + + @ApiProperty({ + example: faker.date.future(), + }) + readonly passwordExpired: Date; + + @ApiProperty({ + example: faker.date.past(), + }) + readonly passwordCreated: Date; + + @ApiProperty({ + example: [1, 0], + }) + readonly passwordAttempt: number; + + @ApiProperty({ + example: faker.date.recent(), + }) + readonly signUpDate: Date; + + @Exclude() + readonly salt: string; + + @ApiProperty({ + description: 'Date created at', + example: faker.date.recent(), + required: true, + }) + readonly createdAt: Date; + + @ApiProperty({ + description: 'Date updated at', + example: faker.date.recent(), + required: false, + }) + readonly updatedAt: Date; + + @Exclude() + readonly deletedAt?: Date; +} diff --git a/src/modules/user/serializations/user.grant-permission.serialization.ts b/src/modules/user/serializations/user.grant-permission.serialization.ts new file mode 100644 index 0000000..2fc98ba --- /dev/null +++ b/src/modules/user/serializations/user.grant-permission.serialization.ts @@ -0,0 +1,24 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UserGrantPermissionSerialization { + @ApiProperty({ + example: 'Bearer', + required: true, + }) + readonly tokenType: string; + + @ApiProperty({ + example: 1660190937231, + description: 'Expire in timestamp', + required: true, + }) + readonly expiresIn: string; + + @ApiProperty({ + example: faker.random.alphaNumeric(30), + description: 'Will be valid JWT Encode string', + required: true, + }) + readonly permissionToken: string; +} diff --git a/src/modules/user/serializations/user.import.serialization.ts b/src/modules/user/serializations/user.import.serialization.ts new file mode 100644 index 0000000..f6fbbaf --- /dev/null +++ b/src/modules/user/serializations/user.import.serialization.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UserImportSerialization { + @ApiProperty({ + description: 'Data extract from excel', + example: [{}, {}], + type: 'array', + }) + extract: Record[]; + + @ApiProperty({ + description: 'Data after validation with dto', + example: [{}, {}], + type: 'array', + }) + dto: Record[]; +} diff --git a/src/modules/user/serializations/user.info.serialization.ts b/src/modules/user/serializations/user.info.serialization.ts new file mode 100644 index 0000000..5f95eb2 --- /dev/null +++ b/src/modules/user/serializations/user.info.serialization.ts @@ -0,0 +1,13 @@ +import { PickType } from '@nestjs/swagger'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { UserPayloadSerialization } from 'src/modules/user/serializations/user.payload.serialization'; + +export class UserInfoSerialization extends PickType(UserPayloadSerialization, [ + '_id', + 'username', + 'rememberMe', + 'loginDate', +] as const) { + readonly role: string; + readonly accessFor: ENUM_AUTH_ACCESS_FOR; +} diff --git a/src/modules/user/serializations/user.list.serialization.ts b/src/modules/user/serializations/user.list.serialization.ts new file mode 100644 index 0000000..262f818 --- /dev/null +++ b/src/modules/user/serializations/user.list.serialization.ts @@ -0,0 +1,39 @@ +import { OmitType } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { UserGetSerialization } from './user.get.serialization'; + +export class UserListSerialization extends OmitType(UserGetSerialization, [ + 'role', + 'photo', + 'passwordExpired', + 'passwordCreated', + 'passwordAttempt', + 'signUpDate', + 'inactiveDate', + 'blockedDate', +] as const) { + @Exclude() + readonly role: string; + + @Exclude() + readonly photo?: AwsS3Serialization; + + @Exclude() + readonly passwordExpired: Date; + + @Exclude() + readonly passwordCreated: Date; + + @Exclude() + readonly passwordAttempt: number; + + @Exclude() + readonly signUpDate: Date; + + @Exclude() + readonly inactiveDate?: Date; + + @Exclude() + readonly blockedDate?: Date; +} diff --git a/src/modules/user/serializations/user.login.serialization.ts b/src/modules/user/serializations/user.login.serialization.ts new file mode 100644 index 0000000..4d39882 --- /dev/null +++ b/src/modules/user/serializations/user.login.serialization.ts @@ -0,0 +1,32 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UserLoginSerialization { + @ApiProperty({ + example: 'Bearer', + required: true, + }) + readonly tokenType: string; + + @ApiProperty({ + example: 1660190937231, + description: 'Expire in timestamp', + required: true, + }) + readonly expiresIn: string; + + @ApiProperty({ + example: faker.random.alphaNumeric(30), + description: 'Will be valid JWT Encode string', + required: true, + }) + readonly accessToken: string; + + @ApiProperty({ + example: faker.random.alphaNumeric(30), + description: 'Will be valid JWT Encode string', + required: true, + }) + @ApiProperty() + readonly refreshToken: string; +} diff --git a/src/modules/user/serializations/user.payload-permission.serialization.ts b/src/modules/user/serializations/user.payload-permission.serialization.ts new file mode 100644 index 0000000..72b12bd --- /dev/null +++ b/src/modules/user/serializations/user.payload-permission.serialization.ts @@ -0,0 +1,20 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { UserGetSerialization } from 'src/modules/user/serializations/user.get.serialization'; + +export class UserPayloadPermissionSerialization extends PickType( + UserGetSerialization, + ['_id'] as const +) { + @ApiProperty({ + example: [faker.name.jobTitle(), faker.name.jobTitle()], + type: 'string', + isArray: true, + }) + @Transform( + ({ value }) => value?.map((val: PermissionEntity) => val.code) ?? [] + ) + readonly permissions: string[]; +} diff --git a/src/modules/user/serializations/user.payload.serialization.ts b/src/modules/user/serializations/user.payload.serialization.ts new file mode 100644 index 0000000..c8c89bb --- /dev/null +++ b/src/modules/user/serializations/user.payload.serialization.ts @@ -0,0 +1,81 @@ +import { faker } from '@faker-js/faker'; +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { Exclude, Expose, Transform } from 'class-transformer'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { UserGetSerialization } from 'src/modules/user/serializations/user.get.serialization'; + +export class UserPayloadSerialization extends OmitType(UserGetSerialization, [ + 'photo', + 'role', + 'isActive', + 'blocked', + 'email', + 'mobileNumber', + 'passwordExpired', + 'passwordCreated', + 'passwordAttempt', + 'signUpDate', + 'inactiveDate', + 'blockedDate', + 'createdAt', + 'updatedAt', +] as const) { + @Exclude() + readonly photo?: AwsS3Serialization; + + @ApiProperty({ + example: faker.datatype.uuid(), + type: 'string', + }) + @Transform(({ obj }) => `${obj.role._id}`) + readonly role: string; + + @ApiProperty({ + example: ENUM_AUTH_ACCESS_FOR.ADMIN, + type: 'string', + enum: ENUM_AUTH_ACCESS_FOR, + }) + @Expose() + @Transform(({ obj }) => obj.role.accessFor) + readonly accessFor: ENUM_AUTH_ACCESS_FOR; + + @Exclude() + readonly isActive: boolean; + + @Exclude() + readonly blocked: boolean; + + @Exclude() + readonly passwordExpired: Date; + + @Exclude() + readonly passwordCreated: Date; + + @Exclude() + readonly passwordAttempt: number; + + @Exclude() + readonly signUpDate: Date; + + @Exclude() + readonly inactiveDate?: Date; + + @Exclude() + readonly blockedDate?: Date; + + @Exclude() + readonly email: Date; + + @Exclude() + readonly mobileNumber?: number; + + readonly rememberMe: boolean; + readonly loginDate: Date; + + @Exclude() + readonly createdAt: number; + + @Exclude() + readonly updatedAt: number; +} diff --git a/src/modules/user/serializations/user.profile.serialization.ts b/src/modules/user/serializations/user.profile.serialization.ts new file mode 100644 index 0000000..7709da3 --- /dev/null +++ b/src/modules/user/serializations/user.profile.serialization.ts @@ -0,0 +1,10 @@ +import { OmitType } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import { UserGetSerialization } from './user.get.serialization'; + +export class UserProfileSerialization extends OmitType(UserGetSerialization, [ + 'passwordAttempt', +] as const) { + @Exclude() + passwordAttempt: number; +} diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts new file mode 100644 index 0000000..57457b7 --- /dev/null +++ b/src/modules/user/services/user.service.ts @@ -0,0 +1,284 @@ +import { Injectable } from '@nestjs/common'; +import { IUserService } from 'src/modules/user/interfaces/user.service.interface'; +import { + IDatabaseCreateOptions, + IDatabaseExistOptions, + IDatabaseFindAllOptions, + IDatabaseFindOneOptions, + IDatabaseOptions, + IDatabaseManyOptions, +} from 'src/common/database/interfaces/database.interface'; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; +import { UserRepository } from 'src/modules/user/repository/repositories/user.repository'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ConfigService } from '@nestjs/config'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; +import { UserCreateDto } from 'src/modules/user/dtos/user.create.dto'; +import { IAuthPassword } from 'src/common/auth/interfaces/auth.interface'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { UserUpdateNameDto } from 'src/modules/user/dtos/user.update-name.dto'; +import { IUserDoc, IUserEntity } from "src/modules/user/interfaces/user.interface"; +import { UserPayloadSerialization } from 'src/modules/user/serializations/user.payload.serialization'; +import { plainToInstance } from 'class-transformer'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { UserPayloadPermissionSerialization } from 'src/modules/user/serializations/user.payload-permission.serialization'; +import { RoleEntity } from "../../role/repository/entities/role.entity"; +import { IPermissionGroup } from "../../permission/interfaces/permission.interface"; + +@Injectable() +export class UserService implements IUserService { + private readonly uploadPath: string; + + constructor( + private readonly userRepository: UserRepository, + private readonly helperDateService: HelperDateService, + private readonly helperStringService: HelperStringService, + private readonly configService: ConfigService + ) { + this.uploadPath = this.configService.get('user.uploadPath'); + } + + async findAll( + find?: Record, + options?: IDatabaseFindAllOptions + ): Promise { + return this.userRepository.findAll(find, { + ...options, + join: true, + }); + } + + async findOneById( + _id: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.userRepository.findOneById(_id, options); + } + + async findOne( + find: Record, + options?: IDatabaseFindOneOptions + ): Promise { + return this.userRepository.findOne(find, options); + } + + async findOneByUsername( + username: string, + options?: IDatabaseFindOneOptions + ): Promise { + return this.userRepository.findOne({ username }, options); + } + + async getTotal( + find?: Record, + options?: IDatabaseOptions + ): Promise { + return this.userRepository.getTotal(find, options); + } + + async create( + { + username, + firstName, + lastName, + email, + mobileNumber, + role, + }: UserCreateDto, + { passwordExpired, passwordHash, salt, passwordCreated }: IAuthPassword, + options?: IDatabaseCreateOptions + ): Promise { + const create: UserEntity = new UserEntity(); + create.username = username; + create.firstName = firstName; + create.email = email; + create.password = passwordHash; + create.role = role; + create.isActive = true; + create.inactivePermanent = false; + create.blocked = false; + create.lastName = lastName; + create.salt = salt; + create.passwordExpired = passwordExpired; + create.passwordCreated = passwordCreated; + create.signUpDate = this.helperDateService.create(); + create.passwordAttempt = 0; + create.mobileNumber = mobileNumber ?? undefined; + + return this.userRepository.create(create, options); + } + + async existByEmail( + email: string, + options?: IDatabaseExistOptions + ): Promise { + return this.userRepository.exists( + { + email: { + $regex: new RegExp(`\\b${email}\\b`), + $options: 'i', + }, + }, + { ...options, withDeleted: true } + ); + } + + async existByMobileNumber( + mobileNumber: string, + options?: IDatabaseExistOptions + ): Promise { + return this.userRepository.exists( + { + mobileNumber, + }, + { ...options, withDeleted: true } + ); + } + + async existByUsername( + username: string, + options?: IDatabaseExistOptions + ): Promise { + return this.userRepository.exists( + { username }, + { ...options, withDeleted: true } + ); + } + + async delete(repository: UserDoc): Promise { + return this.userRepository.softDelete(repository); + } + + async updateName( + repository: UserDoc, + { firstName, lastName }: UserUpdateNameDto + ): Promise { + repository.firstName = firstName; + repository.lastName = lastName; + + return this.userRepository.save(repository); + } + + async updatePhoto( + repository: UserDoc, + photo: AwsS3Serialization + ): Promise { + repository.photo = photo; + + return this.userRepository.save(repository); + } + + async updatePassword( + repository: UserDoc, + { passwordHash, passwordExpired, salt, passwordCreated }: IAuthPassword + ): Promise { + repository.password = passwordHash; + repository.passwordExpired = passwordExpired; + repository.passwordCreated = passwordCreated; + repository.salt = salt; + + return this.userRepository.save(repository); + } + + async active(repository: UserDoc): Promise { + repository.isActive = true; + repository.inactiveDate = undefined; + + return this.userRepository.save(repository); + } + + async inactive(repository: UserDoc): Promise { + repository.isActive = false; + repository.inactiveDate = this.helperDateService.create(); + + return this.userRepository.save(repository); + } + + async blocked(repository: UserDoc): Promise { + repository.blocked = true; + repository.blockedDate = this.helperDateService.create(); + + return this.userRepository.save(repository); + } + + async unblocked(repository: UserDoc): Promise { + repository.blocked = false; + repository.blockedDate = undefined; + + return this.userRepository.save(repository); + } + + async maxPasswordAttempt(repository: UserDoc): Promise { + repository.passwordAttempt = 3; + + return this.userRepository.save(repository); + } + + async increasePasswordAttempt(repository: UserDoc): Promise { + repository.passwordAttempt = ++repository.passwordAttempt; + + return this.userRepository.save(repository); + } + + async resetPasswordAttempt(repository: UserDoc): Promise { + repository.passwordAttempt = 0; + + return this.userRepository.save(repository); + } + + async updatePasswordExpired( + repository: UserDoc, + passwordExpired: Date + ): Promise { + repository.passwordExpired = passwordExpired; + + return this.userRepository.save(repository); + } + + async joinWithRole(repository: UserDoc): Promise { + return repository.populate({ + path: 'role', + localField: 'role', + foreignField: '_id', + model: RoleEntity.name, + }); + } + + async createPhotoFilename(): Promise> { + const filename: string = this.helperStringService.random(20); + + return { + path: this.uploadPath, + filename: filename, + }; + } + + async payloadSerialization( + data: IUserDoc + ): Promise { + return plainToInstance(UserPayloadSerialization, data.toObject()); + } + + async payloadPermissionSerialization( + _id: string, + permissions: IPermissionGroup[] + ): Promise { + const permissionEntity: PermissionEntity[] = permissions + .map((val) => val.permissions) + .flat(1); + return plainToInstance(UserPayloadPermissionSerialization, { + _id, + permissions: permissionEntity, + }); + } + + async deleteMany( + find: Record, + options?: IDatabaseManyOptions + ): Promise { + return this.userRepository.deleteMany(find, options); + } +} diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts new file mode 100644 index 0000000..88e46e9 --- /dev/null +++ b/src/modules/user/user.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { UserRepositoryModule } from 'src/modules/user/repository/user.repository.module'; +import { UserService } from './services/user.service'; +@Module({ + imports: [UserRepositoryModule], + exports: [UserService], + providers: [UserService], + controllers: [], +}) +export class UserModule {} diff --git a/src/router/router.module.ts b/src/router/router.module.ts new file mode 100644 index 0000000..8847459 --- /dev/null +++ b/src/router/router.module.ts @@ -0,0 +1,59 @@ +import { DynamicModule, ForwardReference, Module, Type } from '@nestjs/common'; +import { RouterModule as NestJsRouterModule } from '@nestjs/core'; +import { RoutesAdminModule } from './routes/routes.admin.module'; +import { RoutesCallbackModule } from './routes/routes.callback.module'; +import { RoutesModule } from './routes/routes.module'; +import { RoutesPublicModule } from './routes/routes.public.module'; +import { RoutesTestModule } from './routes/routes.test.module'; + +@Module({}) +export class RouterModule { + static forRoot(): DynamicModule { + const imports: ( + | DynamicModule + | Type + | Promise + | ForwardReference + )[] = []; + + if (process.env.HTTP_ENABLE === 'true') { + imports.push( + RoutesModule, + RoutesTestModule, + RoutesPublicModule, + RoutesAdminModule, + RoutesCallbackModule, + NestJsRouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + { + path: '/test', + module: RoutesTestModule, + }, + { + path: '/public', + module: RoutesPublicModule, + }, + { + path: '/admin', + module: RoutesAdminModule, + }, + { + path: '/callback', + module: RoutesCallbackModule, + }, + ]) + ); + } + + return { + module: RouterModule, + providers: [], + exports: [], + controllers: [], + imports, + }; + } +} diff --git a/src/router/routes/routes.admin.module.ts b/src/router/routes/routes.admin.module.ts new file mode 100644 index 0000000..2a03d1c --- /dev/null +++ b/src/router/routes/routes.admin.module.ts @@ -0,0 +1,31 @@ +import { Module } from '@nestjs/common'; +import { ApiKeyModule } from 'src/common/api-key/api-key.module'; +import { ApiKeyAdminController } from 'src/common/api-key/controllers/api-key.admin.controller'; +import { AuthModule } from 'src/common/auth/auth.module'; +import { SettingAdminController } from 'src/common/setting/controllers/setting.admin.controller'; +import { PermissionAdminController } from 'src/modules/permission/controllers/permission.admin.controller'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { RoleAdminController } from 'src/modules/role/controllers/role.admin.controller'; +import { RoleModule } from 'src/modules/role/role.module'; +import { UserAdminController } from 'src/modules/user/controllers/user.admin.controller'; +import { UserModule } from 'src/modules/user/user.module'; + +@Module({ + controllers: [ + SettingAdminController, + ApiKeyAdminController, + PermissionAdminController, + RoleAdminController, + UserAdminController, + ], + providers: [], + exports: [], + imports: [ + AuthModule, + ApiKeyModule, + PermissionModule, + RoleModule, + UserModule, + ], +}) +export class RoutesAdminModule {} diff --git a/src/router/routes/routes.callback.module.ts b/src/router/routes/routes.callback.module.ts new file mode 100644 index 0000000..a6dd3ac --- /dev/null +++ b/src/router/routes/routes.callback.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +@Module({ + controllers: [], + providers: [], + exports: [], + imports: [], +}) +export class RoutesCallbackModule {} diff --git a/src/router/routes/routes.module.ts b/src/router/routes/routes.module.ts new file mode 100644 index 0000000..55952b0 --- /dev/null +++ b/src/router/routes/routes.module.ts @@ -0,0 +1,37 @@ +import { Module } from '@nestjs/common'; +import { TerminusModule } from '@nestjs/terminus'; +import { AuthModule } from 'src/common/auth/auth.module'; +import { AwsModule } from 'src/common/aws/aws.module'; +import { MessageController } from 'src/common/message/controllers/message.controller'; +import { SettingController } from 'src/common/setting/controllers/setting.controller'; +import { HealthController } from 'src/health/controllers/health.controller'; +import { HealthModule } from 'src/health/health.module'; +import { RoleModule } from 'src/modules/role/role.module'; +import { UserController } from 'src/modules/user/controllers/user.controller'; +import { UserModule } from 'src/modules/user/user.module'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { ApiKeyController } from 'src/common/api-key/controllers/api-key.controller'; +import { ApiKeyModule } from 'src/common/api-key/api-key.module'; + +@Module({ + controllers: [ + HealthController, + SettingController, + MessageController, + UserController, + ApiKeyController, + ], + providers: [], + exports: [], + imports: [ + AwsModule, + TerminusModule, + AuthModule, + HealthModule, + RoleModule, + UserModule, + PermissionModule, + ApiKeyModule, + ], +}) +export class RoutesModule {} diff --git a/src/router/routes/routes.public.module.ts b/src/router/routes/routes.public.module.ts new file mode 100644 index 0000000..81ac8ab --- /dev/null +++ b/src/router/routes/routes.public.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { UserPublicController } from "../../modules/user/controllers/user.public.controller"; +import { UserModule } from "../../modules/user/user.module"; +import { AuthModule } from "../../common/auth/auth.module"; +import { RoleModule } from "../../modules/role/role.module"; +// import { AuthModule } from 'src/common/auth/auth.module'; +// import { RoleModule } from 'src/modules/role/role.module'; +// import { UserPublicController } from 'src/modules/user/controllers/user.public.controller'; +// import { UserModule } from 'src/modules/user/user.module'; + +// @Module({ +// controllers: [UserPublicController], +// providers: [], +// exports: [], +// imports: [UserModule, AuthModule, RoleModule], +// }) +// export class RoutesPublicModule {} + +@Module({ + controllers: [UserPublicController], + providers: [], + exports: [], + imports: [UserModule, AuthModule, RoleModule], +}) +export class RoutesPublicModule {} diff --git a/src/router/routes/routes.test.module.ts b/src/router/routes/routes.test.module.ts new file mode 100644 index 0000000..dc2e46e --- /dev/null +++ b/src/router/routes/routes.test.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +@Module({ + controllers: [], + providers: [], + exports: [], + imports: [], +}) +export class RoutesTestModule {} diff --git a/src/swagger.ts b/src/swagger.ts new file mode 100644 index 0000000..b56145a --- /dev/null +++ b/src/swagger.ts @@ -0,0 +1,82 @@ +import { Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { NestApplication } from '@nestjs/core'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant'; +import { + AwsS3MultipartPartsSerialization, + AwsS3MultipartSerialization, +} from 'src/common/aws/serializations/aws.s3-multipart.serialization'; +import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization'; +import { ResponseDefaultSerialization } from 'src/common/response/serializations/response.default.serialization'; +import { ResponsePagingSerialization } from 'src/common/response/serializations/response.paging.serialization'; + +export default async function (app: NestApplication) { + const configService = app.get(ConfigService); + const env: string = configService.get('app.env'); + const logger = new Logger(); + + const docName: string = configService.get('doc.name'); + const docDesc: string = configService.get('doc.description'); + const docVersion: string = configService.get('doc.version'); + const docPrefix: string = configService.get('doc.prefix'); + + if (env !== ENUM_APP_ENVIRONMENT.PRODUCTION) { + const documentBuild = new DocumentBuilder() + .setTitle(docName) + .setDescription(docDesc) + .setVersion(docVersion) + .addTag("API's") + .addServer(`/`) + .addServer(`/staging`) + .addServer(`/production`) + .addBearerAuth( + { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + 'accessToken' + ) + .addBearerAuth( + { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + 'refreshToken' + ) + .addApiKey( + { type: 'apiKey', in: 'header', name: 'x-api-key' }, + 'apiKey' + ) + .addApiKey( + { + type: 'apiKey', + in: 'header', + name: 'x-permission-token', + description: 'grant permission for /admin prefix endpoints', + }, + 'permissionToken' + ) + .build(); + + const document = SwaggerModule.createDocument(app, documentBuild, { + deepScanRoutes: true, + extraModels: [ + ResponseDefaultSerialization, + ResponsePagingSerialization, + AwsS3MultipartPartsSerialization, + AwsS3MultipartSerialization, + AwsS3Serialization, + ], + }); + + SwaggerModule.setup(docPrefix, app, document, { + explorer: true, + customSiteTitle: docName, + }); + + logger.log( + `==========================================================` + ); + + logger.log(`Docs will serve on ${docPrefix}`, 'NestApplication'); + + logger.log( + `==========================================================` + ); + } +} diff --git a/test/e2e/api-key/api-key.admin.e2e-spec.ts b/test/e2e/api-key/api-key.admin.e2e-spec.ts new file mode 100644 index 0000000..df33b90 --- /dev/null +++ b/test/e2e/api-key/api-key.admin.e2e-spec.ts @@ -0,0 +1,282 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesAdminModule } from 'src/router/routes/routes.admin.module'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant'; +import { + E2E_API_KEY_ADMIN_ACTIVE_URL, + E2E_API_KEY_ADMIN_INACTIVE_URL, + E2E_API_KEY_ADMIN_UPDATE_DATE_URL, +} from 'test/e2e/api-key/api-key.constant'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { faker } from '@faker-js/faker'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity'; +import { + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, +} from 'test/e2e/user/user.constant'; + +describe('E2E Api Key Admin', () => { + let app: INestApplication; + let authService: AuthService; + let apiKeyService: ApiKeyService; + let helperDateService: HelperDateService; + + let accessToken: string; + let permissionToken: string; + + let apiKey: ApiKeyDoc; + let apiKeyExpired: ApiKeyDoc; + const apiKeyCreate = { + name: `${faker.name.firstName()}${faker.random.alphaNumeric(20)}`, + }; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesAdminModule, + RouterModule.register([ + { + path: '/admin', + module: RoutesAdminModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + authService = app.get(AuthService); + apiKeyService = app.get(ApiKeyService); + helperDateService = app.get(HelperDateService); + + const payload = await authService.createPayloadAccessToken( + { + ...E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + loginDate: new Date(), + }, + false + ); + accessToken = await authService.createAccessToken(payload); + permissionToken = await authService.createPermissionToken({ + ...E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, + _id: payload._id, + }); + + const apiKeyCreated1 = await apiKeyService.create( + E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST._id, + { + name: faker.internet.userName(), + description: faker.random.alphaNumeric(), + }); + + apiKey = await apiKeyService.findOneById(apiKeyCreated1.doc._id); + + const apiKeyCreated2 = await apiKeyService.create( + E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST._id, + { + name: faker.internet.userName(), + startDate: helperDateService.backwardInDays(7), + endDate: helperDateService.backwardInDays(1), + }); + + apiKeyExpired = await apiKeyService.findOneById(apiKeyCreated2.doc._id); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await apiKeyService.deleteMany({ + _id: apiKey._id, + }); + + await apiKeyService.deleteMany({ + name: apiKeyCreate.name, + }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_ACTIVE_URL} Active not found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_API_KEY_ADMIN_ACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_ACTIVE_URL} Expired`, async () => { + await apiKeyService.inactive(apiKeyExpired); + const response = await request(app.getHttpServer()) + .patch( + E2E_API_KEY_ADMIN_ACTIVE_URL.replace(':_id', apiKeyExpired._id) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + await apiKeyService.active(apiKeyExpired); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR + ); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_ACTIVE_URL} already Active`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_API_KEY_ADMIN_ACTIVE_URL.replace(':_id', apiKey._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_IS_ACTIVE_ERROR + ); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_INACTIVE_URL} Inactive not found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_API_KEY_ADMIN_INACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_INACTIVE_URL} Expired`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_API_KEY_ADMIN_INACTIVE_URL.replace( + ':_id', + apiKeyExpired._id + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR + ); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_INACTIVE_URL} Inactive Success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_API_KEY_ADMIN_INACTIVE_URL.replace(':_id', apiKey._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_INACTIVE_URL} Inactive already inactive`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_API_KEY_ADMIN_INACTIVE_URL.replace(':_id', apiKey._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_IS_ACTIVE_ERROR + ); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_ACTIVE_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_API_KEY_ADMIN_ACTIVE_URL.replace(':_id', apiKey._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_API_KEY_ADMIN_UPDATE_DATE_URL} Not Found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_API_KEY_ADMIN_UPDATE_DATE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .send({ + startDate: helperDateService.forwardInDays(1), + endDate: helperDateService.forwardInDays(7), + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_API_KEY_ADMIN_UPDATE_DATE_URL} Expired`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_API_KEY_ADMIN_UPDATE_DATE_URL.replace( + ':_id', + apiKeyExpired._id + ) + ) + .send({ + startDate: helperDateService.forwardInDays(1), + endDate: helperDateService.forwardInDays(7), + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR + ); + }); + + it(`PUT ${E2E_API_KEY_ADMIN_UPDATE_DATE_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_API_KEY_ADMIN_UPDATE_DATE_URL.replace(':_id', apiKey._id)) + .send({ + startDate: helperDateService.forwardInDays(1), + endDate: helperDateService.forwardInDays(7), + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/api-key/api-key.constant.ts b/test/e2e/api-key/api-key.constant.ts new file mode 100644 index 0000000..2ab6c52 --- /dev/null +++ b/test/e2e/api-key/api-key.constant.ts @@ -0,0 +1,12 @@ +export const E2E_API_KEY_ADMIN_UPDATE_DATE_URL = + '/admin/api-key/update/:_id/date'; +export const E2E_API_KEY_ADMIN_UPDATE_RESET_URL = '/api-key/update/:_id/reset'; +export const E2E_API_KEY_ADMIN_ACTIVE_URL = '/admin/api-key/update/:_id/active'; +export const E2E_API_KEY_ADMIN_INACTIVE_URL = + '/admin/api-key/update/:_id/inactive'; + + +export const E2E_API_KEY_ADMIN_LIST_URL = '/api-key/list'; +export const E2E_API_KEY_ADMIN_GET_URL = '/api-key/get/:_id'; +export const E2E_API_KEY_ADMIN_CREATE_URL = '/api-key/create'; +export const E2E_API_KEY_ADMIN_UPDATE_NAME_URL = '/api-key/update/:_id'; \ No newline at end of file diff --git a/test/e2e/api-key/api-key.e2e-spec.ts b/test/e2e/api-key/api-key.e2e-spec.ts new file mode 100644 index 0000000..c0f755b --- /dev/null +++ b/test/e2e/api-key/api-key.e2e-spec.ts @@ -0,0 +1,276 @@ +import { HttpStatus, INestApplication } from "@nestjs/common"; +import { AuthService } from "../../../src/common/auth/services/auth.service"; +import { ApiKeyService } from "../../../src/common/api-key/services/api-key.service"; +import { HelperDateService } from "../../../src/common/helper/services/helper.date.service"; +import { ApiKeyDoc } from "../../../src/common/api-key/repository/entities/api-key.entity"; +import { faker } from '@faker-js/faker'; +import * as process from "process"; +import { Test } from "@nestjs/testing"; +import { CommonModule } from "../../../src/common/common.module"; +import { RoutesModule } from "../../../src/router/routes/routes.module"; +import { RouterModule } from "@nestjs/core"; +import { useContainer } from "class-validator"; +import { E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST } from "../user/user.constant"; +import { + E2E_API_KEY_ADMIN_CREATE_URL, + E2E_API_KEY_ADMIN_GET_URL, + E2E_API_KEY_ADMIN_LIST_URL, E2E_API_KEY_ADMIN_UPDATE_NAME_URL, E2E_API_KEY_ADMIN_UPDATE_RESET_URL +} from "./api-key.constant"; +import request from "supertest"; +import { DatabaseDefaultUUID } from "../../../src/common/database/constants/database.function.constant"; +import { ENUM_API_KEY_STATUS_CODE_ERROR } from "../../../src/common/api-key/constants/api-key.status-code.constant"; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from "../../../src/common/request/constants/request.status-code.constant"; +import { response } from "express"; + +describe('E2E Api Key', () => { + let app: INestApplication; + let authService: AuthService; + let apiKeyService: ApiKeyService; + let helperDateService: HelperDateService; + + let accessToken: string; + let permissionToken: string; + + let apiKey: ApiKeyDoc; + let apiKeyExpired: ApiKeyDoc; + + const apiKeyCreate = { + name: `${faker.name.firstName()}${faker.random.alphaNumeric(20)}`, + }; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), {fallbackOnErrors: true}); + authService = app.get(AuthService); + apiKeyService = app.get(ApiKeyService); + helperDateService = app.get(HelperDateService); + + const payload = await authService.createPayloadAccessToken( + { + ...E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + loginDate: new Date() + }, + false + ); + + accessToken = await authService.createAccessToken(payload); + permissionToken = await authService.createPermissionToken({ + ...E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, + _id: payload._id + }); + + const apiKeyCreated1 = await apiKeyService.create( + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST._id, + { + name: faker.internet.userName(), + description: faker.random.alphaNumeric(), + } + ); + + apiKey = await apiKeyService.findOneById(apiKeyCreated1.doc._id); + + const apiKeyCreated2 = await apiKeyService.create( + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST._id, + { + name: faker.internet.userName(), + startDate: helperDateService.backwardInDays(7), + endDate: helperDateService.backwardInDays(1), + } + ); + + apiKeyExpired = await apiKeyService.findOneById(apiKeyCreated2.doc._id); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await apiKeyService.deleteMany({ + _id: apiKey._id, + }); + + await apiKeyService.deleteMany({ + name: apiKeyCreate.name + }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`GET ${E2E_API_KEY_ADMIN_LIST_URL} List Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_API_KEY_ADMIN_LIST_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`GET ${E2E_API_KEY_ADMIN_GET_URL} GET Not Found`, async () => { + console.log(apiKey._id); + const response = await request(app.getHttpServer()) + .get( + E2E_API_KEY_ADMIN_GET_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR + ); + }); + + it(`GET ${E2E_API_KEY_ADMIN_GET_URL} GET Success`, async () => { + const response = await request(app.getHttpServer()) + .get( + E2E_API_KEY_ADMIN_GET_URL.replace(':_id', apiKey._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`POST ${E2E_API_KEY_ADMIN_CREATE_URL} Create Error Request`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_API_KEY_ADMIN_CREATE_URL) + .send({ + name: [1231231] + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR, + ); + }); + + it(`POST ${E2E_API_KEY_ADMIN_CREATE_URL} Create Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_API_KEY_ADMIN_CREATE_URL) + .send(apiKeyCreate) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.CREATED); + expect(response.body.statusCode).toEqual(HttpStatus.CREATED); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_UPDATE_RESET_URL} Reset Not Found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_API_KEY_ADMIN_UPDATE_RESET_URL.replace( + ':_id', + DatabaseDefaultUUID() + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_API_KEY_ADMIN_UPDATE_RESET_URL} Expired`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_API_KEY_ADMIN_UPDATE_RESET_URL.replace( + ':_id', + apiKeyExpired._id + )) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR + ); + }); + + it( `PATCH ${E2E_API_KEY_ADMIN_UPDATE_RESET_URL} Reset Success`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_API_KEY_ADMIN_UPDATE_RESET_URL.replace(':_id', apiKey._id) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_API_KEY_ADMIN_UPDATE_NAME_URL} Error request`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_API_KEY_ADMIN_UPDATE_NAME_URL.replace(':_id', apiKey._id)) + .send({ + name: [], + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`PUT ${E2E_API_KEY_ADMIN_UPDATE_NAME_URL} Not Found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_API_KEY_ADMIN_UPDATE_NAME_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .send({ + name: faker.name.jobArea(), + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_API_KEY_ADMIN_UPDATE_NAME_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_API_KEY_ADMIN_UPDATE_NAME_URL.replace( + ':_id', + apiKey._id + )) + .send({ + name: faker.name.jobArea() + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + +}) \ No newline at end of file diff --git a/test/e2e/jest.json b/test/e2e/jest.json new file mode 100644 index 0000000..e658ae6 --- /dev/null +++ b/test/e2e/jest.json @@ -0,0 +1,30 @@ +{ + "testTimeout": 10000, + "rootDir": "../../", + "modulePaths": [ + "." + ], + "testEnvironment": "node", + "testMatch": [ + "/test/e2e/api-key/*.e2e-spec.ts", + "/test/e2e/permission/*.e2e-spec.ts", + "/test/e2e/role/*.e2e-spec.ts", + "/test/e2e/setting/*.e2e-spec.ts", + "/test/e2e/user/*.e2e-spec.ts" + ], + "collectCoverage": true, + "coverageDirectory": "coverage-e2e", + "collectCoverageFrom": [ + "./src/common/api-key/controllers/**", + "./src/common/setting/controllers/**", + "./src/modules/**/controllers/**" + ], + "moduleFileExtensions": [ + "js", + "ts", + "json" + ], + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/test/e2e/permission/permission.admin.e2e-spec.ts b/test/e2e/permission/permission.admin.e2e-spec.ts new file mode 100644 index 0000000..8310660 --- /dev/null +++ b/test/e2e/permission/permission.admin.e2e-spec.ts @@ -0,0 +1,285 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { + E2E_PERMISSION_ADMIN_ACTIVE_URL, + E2E_PERMISSION_ADMIN_GET_URL, + E2E_PERMISSION_ADMIN_GROUP_URL, + E2E_PERMISSION_ADMIN_INACTIVE_URL, + E2E_PERMISSION_ADMIN_LIST_URL, + E2E_PERMISSION_ADMIN_UPDATE_URL, +} from './permission.constant'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { PermissionService } from 'src/modules/permission/services/permission.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesAdminModule } from 'src/router/routes/routes.admin.module'; +import { ENUM_PERMISSION_STATUS_CODE_ERROR } from 'src/modules/permission/constants/permission.status-code.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; +import { + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, +} from 'test/e2e/user/user.constant'; +import { PermissionUpdateDescriptionDto } from 'src/modules/permission/dtos/permission.update-description.dto'; + +describe('E2E Permission Admin', () => { + let app: INestApplication; + let authService: AuthService; + let permissionService: PermissionService; + + let accessToken: string; + let permissionToken: string; + let permission: PermissionEntity; + + const updateData: PermissionUpdateDescriptionDto = { + description: 'UPDATE_ROLE', + }; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesAdminModule, + RouterModule.register([ + { + path: '/admin', + module: RoutesAdminModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + authService = app.get(AuthService); + permissionService = app.get(PermissionService); + + const payload = await authService.createPayloadAccessToken( + { + ...E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + loginDate: new Date(), + }, + false + ); + accessToken = await authService.createAccessToken(payload); + permissionToken = await authService.createPermissionToken({ + ...E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, + _id: payload._id, + }); + + permission = await permissionService.create({ + code: 'TEST_PERMISSION_XXXX', + description: 'test description', + group: ENUM_PERMISSION_GROUP.PERMISSION, + }); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await permissionService.deleteMany({ + code: 'TEST_PERMISSION_XXXX', + }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`GET ${E2E_PERMISSION_ADMIN_LIST_URL} List Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_PERMISSION_ADMIN_LIST_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`GET ${E2E_PERMISSION_ADMIN_GROUP_URL} Group Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_PERMISSION_ADMIN_GROUP_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`GET ${E2E_PERMISSION_ADMIN_GET_URL} Get Not Found`, async () => { + const response = await request(app.getHttpServer()) + .get( + E2E_PERMISSION_ADMIN_GET_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR + ); + }); + + it(`GET ${E2E_PERMISSION_ADMIN_GET_URL} Get Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_PERMISSION_ADMIN_GET_URL.replace(':_id', permission._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_PERMISSION_ADMIN_UPDATE_URL} Update Not found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_PERMISSION_ADMIN_UPDATE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .send(updateData) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_PERMISSION_ADMIN_UPDATE_URL} Update Error Request`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_PERMISSION_ADMIN_UPDATE_URL.replace(':_id', permission._id) + ) + .send({ + name: [1231231], + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`PUT ${E2E_PERMISSION_ADMIN_UPDATE_URL} Update Success`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_PERMISSION_ADMIN_UPDATE_URL.replace(':_id', permission._id) + ) + .send(updateData) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_PERMISSION_ADMIN_ACTIVE_URL} Active not found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_PERMISSION_ADMIN_ACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_PERMISSION_ADMIN_ACTIVE_URL} already Active`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_PERMISSION_ADMIN_ACTIVE_URL.replace(':_id', permission._id) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_IS_ACTIVE_ERROR + ); + }); + + it(`PATCH ${E2E_PERMISSION_ADMIN_INACTIVE_URL} Inactive not found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_PERMISSION_ADMIN_INACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_PERMISSION_ADMIN_INACTIVE_URL} Inactive Success`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_PERMISSION_ADMIN_INACTIVE_URL.replace( + ':_id', + permission._id + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_PERMISSION_ADMIN_INACTIVE_URL} Inactive already inactive`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_PERMISSION_ADMIN_INACTIVE_URL.replace( + ':_id', + permission._id + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_IS_ACTIVE_ERROR + ); + }); + + it(`PATCH ${E2E_PERMISSION_ADMIN_ACTIVE_URL} Active Success`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_PERMISSION_ADMIN_ACTIVE_URL.replace(':_id', permission._id) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/permission/permission.constant.ts b/test/e2e/permission/permission.constant.ts new file mode 100644 index 0000000..d2b713c --- /dev/null +++ b/test/e2e/permission/permission.constant.ts @@ -0,0 +1,8 @@ +export const E2E_PERMISSION_ADMIN_LIST_URL = '/admin/permission/list'; +export const E2E_PERMISSION_ADMIN_GROUP_URL = '/admin/permission/group'; +export const E2E_PERMISSION_ADMIN_GET_URL = '/admin/permission/get/:_id'; +export const E2E_PERMISSION_ADMIN_UPDATE_URL = '/admin/permission/update/:_id'; +export const E2E_PERMISSION_ADMIN_ACTIVE_URL = + '/admin/permission/update/:_id/active'; +export const E2E_PERMISSION_ADMIN_INACTIVE_URL = + '/admin/permission/update/:_id/inactive'; diff --git a/test/e2e/role/role.admin.e2e-spec.ts b/test/e2e/role/role.admin.e2e-spec.ts new file mode 100644 index 0000000..659cea6 --- /dev/null +++ b/test/e2e/role/role.admin.e2e-spec.ts @@ -0,0 +1,498 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { + E2E_ROLE_ACCESS_FOR_URL, + E2E_ROLE_ADMIN_ACTIVE_URL, + E2E_ROLE_ADMIN_CREATE_URL, + E2E_ROLE_ADMIN_DELETE_URL, + E2E_ROLE_ADMIN_GET_BY_ID_URL, + E2E_ROLE_ADMIN_INACTIVE_URL, + E2E_ROLE_ADMIN_LIST_URL, + E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL, + E2E_ROLE_ADMIN_UPDATE_URL, +} from './role.constant'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { PermissionService } from 'src/modules/permission/services/permission.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesAdminModule } from 'src/router/routes/routes.admin.module'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { RoleCreateDto } from 'src/modules/role/dtos/role.create.dto'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; +import { RoleEntity } from 'src/modules/role/repository/entities/role.entity'; +import { PermissionEntity } from 'src/modules/permission/repository/entities/permission.entity'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, +} from 'test/e2e/user/user.constant'; +import { ENUM_PERMISSION_STATUS_CODE_ERROR } from 'src/modules/permission/constants/permission.status-code.constant'; +import { RoleUpdateNameDto } from 'src/modules/role/dtos/role.update-name.dto'; +import { RoleUpdatePermissionDto } from 'src/modules/role/dtos/role.update-permission.dto'; + +describe('E2E Role Admin', () => { + let app: INestApplication; + let authService: AuthService; + let roleService: RoleService; + let permissionService: PermissionService; + + let role: RoleEntity; + let roleUpdate: RoleEntity; + + let accessToken: string; + let permissionToken: string; + + let successData: RoleCreateDto; + let updateData: RoleUpdateNameDto; + let updateDataPermission: RoleUpdatePermissionDto; + let existData: RoleCreateDto; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesAdminModule, + RouterModule.register([ + { + path: '/admin', + module: RoutesAdminModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + authService = app.get(AuthService); + roleService = app.get(RoleService); + permissionService = app.get(PermissionService); + + const permissions: PermissionEntity[] = await permissionService.findAll( + { + code: { + $in: [ + ENUM_AUTH_PERMISSIONS.ROLE_READ, + ENUM_AUTH_PERMISSIONS.ROLE_CREATE, + ENUM_AUTH_PERMISSIONS.ROLE_UPDATE, + ENUM_AUTH_PERMISSIONS.ROLE_DELETE, + ENUM_AUTH_PERMISSIONS.PERMISSION_READ, + ENUM_AUTH_PERMISSIONS.PERMISSION_READ, + ENUM_AUTH_PERMISSIONS.PERMISSION_READ, + ENUM_AUTH_PERMISSIONS.PERMISSION_READ, + ], + }, + } + ); + + successData = { + name: 'testRole1', + permissions: permissions.map((val) => `${val._id}`), + accessFor: ENUM_AUTH_ACCESS_FOR.ADMIN, + }; + + roleUpdate = await roleService.create({ + name: 'testRole2', + permissions: permissions.map((val) => `${val._id}`), + accessFor: ENUM_AUTH_ACCESS_FOR.ADMIN, + }); + + updateData = { + name: 'testRole3', + }; + + updateDataPermission = { + permissions: permissions.map((val) => `${val._id}`), + accessFor: ENUM_AUTH_ACCESS_FOR.ADMIN, + }; + + existData = { + name: 'testRole', + permissions: permissions.map((val) => `${val._id}`), + accessFor: ENUM_AUTH_ACCESS_FOR.ADMIN, + }; + + role = await roleService.create(existData); + + const payload = await authService.createPayloadAccessToken( + { + ...E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + loginDate: new Date(), + }, + false + ); + accessToken = await authService.createAccessToken(payload); + permissionToken = await authService.createPermissionToken({ + ...E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, + _id: payload._id, + }); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await roleService.deleteMany({ + name: 'testRole', + }); + await roleService.deleteMany({ + name: 'testRole1', + }); + await roleService.deleteMany({ + name: 'testRole2', + }); + await roleService.deleteMany({ + name: 'testRole3', + }); + } catch (e) {} + + await app.close(); + }); + + it(`GET ${E2E_ROLE_ADMIN_LIST_URL} List Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_ROLE_ADMIN_LIST_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`GET ${E2E_ROLE_ADMIN_GET_BY_ID_URL} Get Not Found`, async () => { + const response = await request(app.getHttpServer()) + .get( + E2E_ROLE_ADMIN_GET_BY_ID_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR + ); + }); + + it(`GET ${E2E_ROLE_ADMIN_GET_BY_ID_URL} Get Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_ROLE_ADMIN_GET_BY_ID_URL.replace(':_id', role._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`POST ${E2E_ROLE_ADMIN_CREATE_URL} Create Error Request`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_ROLE_ADMIN_CREATE_URL) + .send({ + name: 123123, + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`POST ${E2E_ROLE_ADMIN_CREATE_URL} Create Exist`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_ROLE_ADMIN_CREATE_URL) + .send(existData) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_EXIST_ERROR + ); + }); + + it(`POST ${E2E_ROLE_ADMIN_CREATE_URL} Create Permission Not Found`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_ROLE_ADMIN_CREATE_URL) + .send({ + ...successData, + permissions: [DatabaseDefaultUUID()], + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR + ); + }); + + it(`POST ${E2E_ROLE_ADMIN_CREATE_URL} Create Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_ROLE_ADMIN_CREATE_URL) + .send(successData) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.CREATED); + expect(response.body.statusCode).toEqual(HttpStatus.CREATED); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_URL} Update Error Request`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_ROLE_ADMIN_UPDATE_URL.replace(':_id', roleUpdate._id)) + .send({ + name: [231231], + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_URL} Update Not found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_ROLE_ADMIN_UPDATE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .send(updateData) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_URL} Update Exist`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_ROLE_ADMIN_UPDATE_URL.replace(':_id', roleUpdate._id)) + .send(existData) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_EXIST_ERROR + ); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_URL} Update Success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_ROLE_ADMIN_UPDATE_URL.replace(':_id', roleUpdate._id)) + .send(updateData) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL} Update Error Request`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL.replace( + ':_id', + roleUpdate._id + ) + ) + .send({ + name: [231231], + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL} Update Not found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .send(updateDataPermission) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL} Update Permission Not Found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL.replace( + ':_id', + roleUpdate._id + ) + ) + .send({ + accessFor: ENUM_AUTH_ACCESS_FOR.ADMIN, + permissions: [DatabaseDefaultUUID()], + }) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_PERMISSION_STATUS_CODE_ERROR.PERMISSION_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL} Update Success`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL.replace( + ':_id', + roleUpdate._id + ) + ) + .send(updateDataPermission) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_ROLE_ADMIN_INACTIVE_URL} Inactive, Not Found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_ROLE_ADMIN_INACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_ROLE_ADMIN_INACTIVE_URL} Inactive, success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_ROLE_ADMIN_INACTIVE_URL.replace(':_id', roleUpdate._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_ROLE_ADMIN_INACTIVE_URL} Inactive, already inactive`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_ROLE_ADMIN_INACTIVE_URL.replace(':_id', roleUpdate._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_IS_ACTIVE_ERROR + ); + }); + + it(`PATCH ${E2E_ROLE_ADMIN_ACTIVE_URL} Active, Not Found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_ROLE_ADMIN_ACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_ROLE_ADMIN_ACTIVE_URL} Active, success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_ROLE_ADMIN_ACTIVE_URL.replace(':_id', roleUpdate._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_ROLE_ADMIN_ACTIVE_URL} Active, already active`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_ROLE_ADMIN_ACTIVE_URL.replace(':_id', roleUpdate._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_IS_ACTIVE_ERROR + ); + }); + + it(`DELETE ${E2E_ROLE_ADMIN_DELETE_URL} Delete Not Found`, async () => { + const response = await request(app.getHttpServer()) + .delete( + E2E_ROLE_ADMIN_DELETE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR + ); + }); + + it(`DELETE ${E2E_ROLE_ADMIN_DELETE_URL} Delete Success`, async () => { + const response = await request(app.getHttpServer()) + .delete(E2E_ROLE_ADMIN_DELETE_URL.replace(':_id', role._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`GET ${E2E_ROLE_ACCESS_FOR_URL} Access For Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_ROLE_ACCESS_FOR_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/role/role.constant.ts b/test/e2e/role/role.constant.ts new file mode 100644 index 0000000..869cf37 --- /dev/null +++ b/test/e2e/role/role.constant.ts @@ -0,0 +1,10 @@ +export const E2E_ROLE_ADMIN_LIST_URL = '/admin/role/list'; +export const E2E_ROLE_ADMIN_GET_BY_ID_URL = '/admin/role/get/:_id'; +export const E2E_ROLE_ADMIN_CREATE_URL = '/admin/role/create'; +export const E2E_ROLE_ADMIN_UPDATE_URL = '/admin/role/update/:_id'; +export const E2E_ROLE_ADMIN_UPDATE_PERMISSION_URL = + '/admin/role/update/:_id/permission'; +export const E2E_ROLE_ADMIN_DELETE_URL = '/admin/role/delete/:_id'; +export const E2E_ROLE_ADMIN_INACTIVE_URL = '/admin/role/update/:_id/inactive'; +export const E2E_ROLE_ADMIN_ACTIVE_URL = '/admin/role/update/:_id/active'; +export const E2E_ROLE_ACCESS_FOR_URL = '/admin/role/access-for'; diff --git a/test/e2e/setting/setting.admin.e2e-spec.ts b/test/e2e/setting/setting.admin.e2e-spec.ts new file mode 100644 index 0000000..6cd9b47 --- /dev/null +++ b/test/e2e/setting/setting.admin.e2e-spec.ts @@ -0,0 +1,181 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { RouterModule } from '@nestjs/core'; +import { Test } from '@nestjs/testing'; +import { useContainer } from 'class-validator'; +import { E2E_SETTING_ADMIN_UPDATE_URL } from './setting.constant'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesAdminModule } from 'src/router/routes/routes.admin.module'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_SETTING_STATUS_CODE_ERROR } from 'src/common/setting/constants/setting.status-code.constant'; +import { SettingService } from 'src/common/setting/services/setting.service'; +import { SettingEntity } from 'src/common/setting/repository/entities/setting.entity'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, +} from 'test/e2e/user/user.constant'; + +describe('E2E Setting Admin', () => { + let app: INestApplication; + let settingService: SettingService; + let authService: AuthService; + + let setting: SettingEntity; + const settingName: string = faker.random.alphaNumeric(10); + + let accessToken: string; + let permissionToken: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesAdminModule, + RouterModule.register([ + { + path: '/admin', + module: RoutesAdminModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + authService = app.get(AuthService); + settingService = app.get(SettingService); + + const payload = await authService.createPayloadAccessToken( + { + ...E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + loginDate: new Date(), + }, + false + ); + accessToken = await authService.createAccessToken(payload); + permissionToken = await authService.createPermissionToken({ + ...E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, + _id: payload._id, + }); + + await settingService.create({ + name: settingName, + value: 'true', + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + }); + setting = await settingService.findOneByName(settingName); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await settingService.deleteMany({ name: settingName }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`PUT ${E2E_SETTING_ADMIN_UPDATE_URL} Update Not Found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_SETTING_ADMIN_UPDATE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ value: 'true', type: ENUM_SETTING_DATA_TYPE.BOOLEAN }); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_SETTING_STATUS_CODE_ERROR.SETTING_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_SETTING_ADMIN_UPDATE_URL} Update Error Request`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_SETTING_ADMIN_UPDATE_URL.replace(':_id', `${setting._id}`)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + value: { test: 'aaa', type: ENUM_SETTING_DATA_TYPE.STRING }, + }); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`PUT ${E2E_SETTING_ADMIN_UPDATE_URL} Update Value Not Allowed`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_SETTING_ADMIN_UPDATE_URL.replace(':_id', `${setting._id}`)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + value: 'test', + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + }); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_SETTING_STATUS_CODE_ERROR.SETTING_VALUE_NOT_ALLOWED_ERROR + ); + }); + + it(`PUT ${E2E_SETTING_ADMIN_UPDATE_URL} Update String Success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_SETTING_ADMIN_UPDATE_URL.replace(':_id', `${setting._id}`)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ value: 'test', type: ENUM_SETTING_DATA_TYPE.STRING }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_SETTING_ADMIN_UPDATE_URL} Update Number Success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_SETTING_ADMIN_UPDATE_URL.replace(':_id', `${setting._id}`)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ value: 123, type: ENUM_SETTING_DATA_TYPE.NUMBER }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_SETTING_ADMIN_UPDATE_URL} Update String Convert If Possible Success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_SETTING_ADMIN_UPDATE_URL.replace(':_id', `${setting._id}`)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ value: 'false', type: ENUM_SETTING_DATA_TYPE.BOOLEAN }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_SETTING_ADMIN_UPDATE_URL} Update Boolean Success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_SETTING_ADMIN_UPDATE_URL.replace(':_id', `${setting._id}`)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ value: false, type: ENUM_SETTING_DATA_TYPE.BOOLEAN }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/setting/setting.constant.ts b/test/e2e/setting/setting.constant.ts new file mode 100644 index 0000000..f64b415 --- /dev/null +++ b/test/e2e/setting/setting.constant.ts @@ -0,0 +1,6 @@ +export const E2E_SETTING_COMMON_LIST_URL = '/setting/list'; +export const E2E_SETTING_COMMON_GET_URL = '/setting/get/:_id'; +export const E2E_SETTING_COMMON_GET_BY_NAME_URL = + '/setting/get/name/:settingName'; + +export const E2E_SETTING_ADMIN_UPDATE_URL = '/admin/setting/update/:_id'; diff --git a/test/e2e/setting/setting.e2e-spec.ts b/test/e2e/setting/setting.e2e-spec.ts new file mode 100644 index 0000000..2a12980 --- /dev/null +++ b/test/e2e/setting/setting.e2e-spec.ts @@ -0,0 +1,126 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { RouterModule } from '@nestjs/core'; +import { Test } from '@nestjs/testing'; +import { useContainer } from 'class-validator'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { + E2E_SETTING_COMMON_GET_BY_NAME_URL, + E2E_SETTING_COMMON_GET_URL, + E2E_SETTING_COMMON_LIST_URL, +} from './setting.constant'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; +import { ENUM_SETTING_STATUS_CODE_ERROR } from 'src/common/setting/constants/setting.status-code.constant'; +import { SettingService } from 'src/common/setting/services/setting.service'; +import { SettingEntity } from 'src/common/setting/repository/entities/setting.entity'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; + +describe('E2E Setting', () => { + let app: INestApplication; + let settingService: SettingService; + + let setting: SettingEntity; + const settingName: string = faker.random.alphaNumeric(10); + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + settingService = app.get(SettingService); + + await settingService.create({ + name: settingName, + value: 'true', + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + }); + setting = await settingService.findOneByName(settingName); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await settingService.deleteMany({ name: settingName }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`GET ${E2E_SETTING_COMMON_LIST_URL} List Success`, async () => { + const response = await request(app.getHttpServer()).get( + E2E_SETTING_COMMON_LIST_URL + ); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`GET ${E2E_SETTING_COMMON_GET_URL} Get Not Found`, async () => { + const response = await request(app.getHttpServer()).get( + E2E_SETTING_COMMON_GET_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_SETTING_STATUS_CODE_ERROR.SETTING_NOT_FOUND_ERROR + ); + }); + + it(`GET ${E2E_SETTING_COMMON_GET_URL} Get Success`, async () => { + const response = await request(app.getHttpServer()).get( + E2E_SETTING_COMMON_GET_URL.replace(':_id', `${setting._id}`) + ); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`GET ${E2E_SETTING_COMMON_GET_BY_NAME_URL} Not Found`, async () => { + const response = await request(app.getHttpServer()).get( + E2E_SETTING_COMMON_GET_BY_NAME_URL.replace( + ':settingName', + faker.name.firstName() + ) + ); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_SETTING_STATUS_CODE_ERROR.SETTING_NOT_FOUND_ERROR + ); + }); + + it(`GET ${E2E_SETTING_COMMON_GET_BY_NAME_URL} Success`, async () => { + const response = await request(app.getHttpServer()).get( + E2E_SETTING_COMMON_GET_BY_NAME_URL.replace( + ':settingName', + setting.name + ) + ); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/files/import.csv b/test/e2e/user/files/import.csv new file mode 100644 index 0000000..d391bea --- /dev/null +++ b/test/e2e/user/files/import.csv @@ -0,0 +1,2 @@ +username,email,firstName,lastName,mobileNumber +test111,test111@mail.com,test111,tesssst,6281276542233 diff --git a/test/e2e/user/files/medium.jpg b/test/e2e/user/files/medium.jpg new file mode 100644 index 0000000000000000000000000000000000000000..60b5d528f05e379420d27012858c68236ba91ce2 GIT binary patch literal 308701 zcmb@tWmH_jvoAWhyK8WF_n^bzE`z%dGB|_;cXt@v-Q9w_yC)FbLeN0$@jvIib?%3E zKiylsd-ZQs)#|R@yHh6D=|Golnm4Qk?02~|~0Q8ms{|@0PmE`2iHMKQ>N~#KP z0ssIW)5RL%36Bc^KwQ0`+KRF?hF~KalwANEfDC{DUwl~E|1UtdvGufm6a9Y6X4dXdF8~0}+wPkR0KuB?KgIuD^0M`NvmXFZ zG6Vqr)2IJk`~URS|L_oq-%Q|t`xa&Y!*eSE07M?&_{smnv&sPgIwJr8((V7@F%$s+ zIN<<5$AYyt)aSq30Fb|xGjLM~)<~(S&KOU)?}@Z11ZaP-Ci9Vsrvj0*d`+*N-2;$f znsYzYIerdW89C?q_wU~hARGXXfPjFAfQW>Ii2NTyMMXhDMa4oxe@j>dnAn(a4*?z_ z9?n~)CM6;wrKYE%q@rhIWMmWM6I4(zf%`wL;NLI+4;2Z35{v*x2Y|8*#+~!i$BbSzF*nT>u4}!*o>Qp$L6yb9 zl|IOtbJYZF(wgEUBuyI9lYKdPx8+&VK#|}J^J3S!j`61c{qo9Jld%qX#ZlkRXO8kM zeCmXMde+Edw17HRf(Ys0C>X_n^*=lja*fpVtn_lcyS(4Cc?)MhVRI&8nkG`z@TGk} zr7W*DE{>7SWZB)+2YMpNK*eigyFp!%hYW^7Udu9HE_O7ge__XXI~!MGb~l%I)=zv` zVZyy-(wck{3PT5}Cc$$wb9~5Qj4J}ly?2ZzCf7!-nEjwWD?&A?oJk^Itvy7mydN%h zVYXh`{5|yP6yN7d#v?Nm`g{9B-PG(_w)YM{YE2M>@qCgoBT7N28r!^}s!=9T9ik07 z8ir%+=u#&ABE^JW4K8&)#HOyUXJY;}(CQzci2HZPDDv)mgQN5cI7Ycy9fO#B7xe zJ9*wW5hqq^=crtUhS?B_yRD0R%r{(AY&Kk^TaT$d*m^U}G)~%Q*QIrz%FZ9fq*N<$ zdz+*1Z8)Dk^ti@-E_LZ!^HZIqL^w5!@SC@gr&;+B0F>$ zOh*i=mUg+^wXX7MQ#~=bRXf`C=wJ?0w9Xts<5r4L+3JsTXRl0SdWX?CmM?S5$G*Re zU!8>bLUwEvmq0r3F>;Xyar*0Hj%hjuI?x66+dmYoo~?>H$(C&Vu83hG@(ijc1;B6W zWT^6WXGU37I`n3Hh?tHw7L$taw|GRqXFr-^p6x!?!ZRe6J^HgEx2fW$U!hhvyT?oC zFLvWOeuUq)4oMYc@sL2)>WKvsn9TAPb@tIFXWfmuxMm6=+m-gP7v8y52Np(%ch0_g zD;1(Y7I3EH1ou>!_8#bK^G2upI%mfdzDWTaB+K|dj9Z4u;qf^D% zFsgfK<4dn(bCOh{yK1gc3mbxYu1sac5mLn#!nRuDX>R@jU;soE*)&$hjQ*U*mWxYq zVgf;lHQKgS_u~-gs&XB%Anj& zFSa&CTqR*lSQfefI_8bjYGa73I7u=o-s=0&30v_|U=*l%eW=XbAwE7;k2|%>M;uMR z;Lf>^XxE7hwEAGh((q8)Xk$rOR7Jq3n;f}$e~%zet{0QOSB5 zxAqY;O+l0)WdnBSA(3Ca3?mY@wv=nAp(tOw`eqPCqCm5H1N6*ms&&m9m2(wM1g*ps z9Xan54LPx%K@~~FU6Jd2b~!m)Qa}qyfk;^z!|{VFBD^-5TS`?%`9LRfoRqp(9XqMz zO#StWSRW=a&3NG~X!i66 z#Zvea{V}Dnd{pVh65$L+F&rVx9?oB1siTAJ16P6>YT0Y2P$R1Nc^KS`H^ZZshVz^- z*^)>2q+!J|MLET&0!%4Ixo@NQ*rc;@sl|uWg6Pw}Ao5Q=Kh1kSEL461v|oPEl&0A- zvwM(6q#8s(?2UCLxu<6uJdpzyn0`IOKQ7}F8SrO{HW zlr34-NL|BbeHOFMOT)@OX|N0f4f zwxO8BuacBAbZ-ok64E~nQEgM}rz>9f3qa4%x_NW#90c<=ZnyZCcOR`60(K4~KNChX z5$f?npGqhU3&3&h^t#b1mI%41=1xtAyddSP6(sY;Q|U*az19W!sxpw(J9dq$fR~dsZ=db zhQGNj3h0o~iz}(Ll*64Cf7pxobV@oe_!~7T-NcVpZM8YOk(IJ#*-o#TVH?S$wE{ z2>Jy?LZZ~rNXw>af(t~9IzR^b5VYXn%Sb!Oq7g=i(TKwZpG@<~oU}-%TjEoP$U2M^ zxlO;r0p{MgQr?&gpBId@^xMa+w^!E90`dU1iSVNcs)HwAc}fAOD80L4W$_R9L zl%nKqT@^@&`PC}r{`Ag)cRaTs-B+ZzD(d)Cv&cj5YhUCI9J!NW!#KgS6yhNz2p7qVre9{* zJ_dg)WQKp{D4%+X1IEesat{?1j}|i}!WvW8{LyJwY5Ft0N1*3q!^hKJ`97S0%mrv| zyiuiJhW(v7b-b#3`nil72?7``#}{mLbBHM4E8}NlM%3JaG$aHItUhIcJE>M|4174n z*h3T5SbM9CZ^9}KU~V2LQNF7Ix?3_!ecu+nw`|c!Gsm*i@j177rCS|+tm-QWW917y z($R!Qb=nS*)R~5BfN3=P481A(E3pdn$~4o6*;9lGG)rrBPMY-^y8UD-4l@>w(ioQ> zrWk?xx*|P8#GFxG$t=|=PTVAF_XcV&{{W!o{H3Jo8AJS5SAuvq?P<<3$}ru{9y#Rb zO?7hmukYoNWh&9xu9D>Qb3{Hyl@}T&^vJCm`o2STSCDDEZ2Z2r>TDyyL(mtJv|sWxi83=O-Z|yF!TZJXouih zW07m6;`7ah0ca*YfN9uIX42}2EkZyN3d?<2Gt4HXbvSTrZkLlfNpVY~6W#Mvi3Gyq zd0({!xP=1bi0~J-^GmVN+dSz!Uwim8u|?LNGAj^`nlkLf}dK| zSj@X$lrZb>>Tds3d@o`)sEHJvDZ5kyx+9@U`Yip`Y#NF`4$@97NQa*k$Y5Qe&JXpv zAc&#~i@{W4hEFxbM6{CSn+rjmi6^Xy)~q(B&tq=mPTJD7E@cm9c3&@#xUitGF9ojz1#C+vDpx_W&Y2C2 z;F&DW_tplG0fye{Rfv#uspHdhBR*Kw=`?k7djtm-0Duedfx^JvnXi3V=!SF;-fwq1=y$=k?%wGer zi)dxUcm)7f=N6oE7}fRlz&sl#lbl&K%668VTqXrEAhTYjaITgsJzgww{29)25U5xr zjvt*Tf=Tl^0V}H9kI$=f+mofjT|SXzanQfF8l*TXg)ZAaEJ zHZ6iLLs}y^n?VlA5(&xha1dbV6zE!c7(l`FSjsK&By=7oNs^m_ETn{QHy%8BpHSp& zq^*5qMhKrWI{uAJFXl6p1D+PtSAs^nBu5SZJ0*jz8CQt{TPj!OB)<1&f1Jlq^Tgsy z5Fy=m6G@W@ydY>cx}Q%1)YnY>l_6WUT4|0Zby6Tayh2hOUMCL(MU@C13zjYmZ_Yxq z;6a)}4K-<^)*h`SE>aKdtPPARp3R*lwW%&y2~#2Umn0R;H`)BhTVeB&IsCv+{ z?RGNZGXVkLV0)e21kz3oi(D)Gc(mM2YVOALuxZ$;x-PRRr_M zv)T(ev^eOvvh@Cy`a)J9C3p)E zJJ1Q!5DIjZw9%2p!EYzwpd9FS zc0cJZYfYDuQAB`aHyo@oMz57bs@Lhdh2fV6q}Pgb54RGB;@Vjt47XWMsjAnpF+CM~ zcoms0jZ)1F-!Q0Kz1KKeO{Ou`8rsj7DZ-Yo$aKl&uuPLL(2BqiLY3x;DbD2aE{0Qj z8z0J9wcN)qX2hxvUMbWs%U%1{#{_!KU^6tF3-SWiiAT!D%?v_Fi+qAszj=8w&m!*$ zv61K4n1o_QN-$7kFl7a+GIF-|$(p^CyK30Y<2JjqIz$3+cuP7koKO+rm6ZeR@Dzbr z3fK&}a)XsT!59p~N*rO)(lW@YU&~ID8)TS?NQ*E9@dquPRN%_9I8YE%WgY050-ZAD ztC>??+~f+`16_PdemQ;4Rp{2UwF82naJCPK1e@9Wr+WpPH zyUTSbpQzL?oT_eg=u%f-Gl7QabEHhK%8LxNK|dv~3oS>SP0kaBGHi#0#+M(56|)TC zrgxP&{jKk08z3ofbA8pnENjAT7p}M)MYXa>L#f818C|5dlF4>YK1$r&C8NZ2(bOG1 z0n854gdr|JxAODU!m z@zrS4Q`K08r^zhRb7bAiQY|&m8J1I~SPoT)!9}u$m|R@|(zmq~j7yUw4IbPk237E- zvkTfuRehNL2heK?=i)T!{}D$FXXRXy*60Fan$2X$M9qYiI+AM{rVW@}7ufC0Q>B<~ z>7F&1Z&S_sS|sxvul{h;epV&`Y3F3H*~$wj&p*sydWfYG?#6S^ax`yOY*Yz-$nD^EGT*HWJBqCP_c3mV6}!$4gtRSTvLND)7SW4|Ir=PGtWNiV^;5ZjrjehhC> zw%M>nd&_hIE+B%2gQf-zki3LfC9C-)3!jBJIz^>3oQzx&hN>M&)CSoC9-PbdCPfFv zh3%?tUQ;|$EBVJO8YWsMX!@=#rzDB8{t@lHkLljYy8MEbw{|XZ+WYEu&Yk|fdLS#q zeL222JGs4*bXM-bjvr}E(lrZ*1{WU}#WGyoVwYRS&Xll!A$QVWnc(#k2Ylj$ZZU3p zR;e$YCMQ|Kwg+W`;rD8Cp$k#6;3xTnZ9h~_8R!%qVYgT-2LF*UiCR#CsbOA0{&;`$ z#FBpyqO>g<6Y*mDX+^W;0_M#l34El=I#`aQ*(O&xbW$K2jbk`FU2C*99b7I8wgcb~ zT4)m!AU3tzakntY&^V-Y_+r6o4Hn{)e(`-)vQCY?N^PDi{_v+R?e%qNN$)+blF6R# zD>I>DC+=|_zJ1Nllf=A}o~s#n6sejsgxm+H7|;9WmB5GQV7S1{}Bnr4%7<99(^)8=vNZO}qVaqO~y`1rHFOytg|Zt`a& z{KWRRUftTmtZ$$D?jH+haYVm6COCg6RB0^3$@^4XPdV_w1k(S){f=TDb(&c^dsC&@ ziXMI$Rn{mxxoIua7CioaE6bcU&!9}{j-*hM8}^EX5|SU8aL$u$UY!jtO75prif~iJ z#U(Wf(+;NNLa@{Z!H*l-hcs11O!cwgEe@S%#Y;*w(-6OBuXg5kb$^gDm&S7~X0a9E z2vcgdQdgUCNd?SsToW zRHH^}@J7##Y_v%^C(irc61|!&{)=w&nHvhsJrMd`|B$3Q{CH4d=aM=QiG&kP+5Jbn z7ba$qOP=NE*{VQZP=tRL+Zspk4V9aOQPQngqB&hNRD)xbBk2lp0$n5{usmUZAa+2M zyz*$Ef}L5I6Vti&gIUSxk@X@O-{^Qmf+M>ghWQIs;lEa2|GL=XW_4;t<|caC`7vU- z6MRPKGt`3&{38}08Qh0i%2S|fo0P_&nrV;#wd49EJL7}+xqd7%i+hx(rBWOVj&6KR zuv*`4?cxs7hn%PA7eO7#^>@1+F6-yo8Q=leq8KUYjGTl!c?y{LW0;l3v@Au1$h$nM zAb>5q_ok**(fRGiuTZmeRAhDS_8t0Pt_+d}6Uj)${`H#9^B)1ZYWqa^5(b`qhr%6qZWr}}MT z={TtF3kK0!q@2(GVvXc6@T6xyB;T%*^Iq%cf*hk1O1E6r<*!UVf8B|ziz-~s^fXZN z6pGL11W#FKKTX8ME4SI}PyQZJ_)a3p?OhFLM?#pIz*+)iUI$rM>sIQwIXlWbkV+D* zwvnj7mY>?e5c@TwQXdGsoSDJ=b}N)iEa~VUz|m23_ORY5t4?#&x+=CMX4mOjsAOzGhkg7gxDgMHA=79+qm>H{ab)EK+5V_EQUArMJy&Y#f7^DcWj`XhoZxgaGc%Q zztCj=>sigK@fs_o)jaO^d4d^6bW1S5V^K$X4EZhm*?nu zx_QQI9KQ&!BqjYBu>H06UVu7+_K}sbn)QBf-w#DLv{rOZ?UgV9!{C5sDaR)hiwz@% zUpenmBD(#i@_b~Sgn?QE!b)yV&Q;b-sN6HW6CH9uA;A}SBhJ>+B8dY_O}7l}9z-58 zL1RjzRUp+1Mv8Be1_$f211)IwnT^XM5jxStD^=;xA!_UJhr}i zSezzN$8&B^-NeBQJVrzl!8!{Ze;rlz7fYXB=Bbq+Jr?yDd^Nd*2H|EO?;zPGE6x!K zHX9fPLT`VZklkst;_|Pe#%RTI)U)WH5h_yJ>Q{^fw6tyZ1@Lz;`!Nu2FV9p%-A-Mb zPF%yDzi{7Gd@uWh^zfeN>^*l7_?YC&@P5cGP9^t5#>>nGWfF?|E6QO0m_b>{{=?(W z(Z@tM)DJh^=onhaZSF3z^DGC57MRfl(hNc#TSL_P}EP{VcXh|l6->S7Ym$J{V~SQI!67oIq! zgA*MN!`P!a9BLWNlYnDrF^U(K&7l*zzH8?yruRL@aUb*gu7aqRpV>G)AoH%xLUy6O z<3!ArI{o_R$%4E!5hIm-0a~)wV_(oXuk-z{j}y7Pj?0+?|!vTzwU646x#XyL9J;+{r5~9j;O-C;0aH1g*2gY98ErB9fA0aLYv4r3L^#rv-SKBxaD#U-4QE9L ztH{1uZbI{PiEt2kEfRi!O*Dz8TqFg3vL_5Tg%^{m&T2QwQ{E}*LG_z|xgL2e5cNly z?G&u_R;yGpJ?`$fc}f_=U;!F1U(2)Y>G`@JH?rH+RI6-BlciP5Dv3GWmtt_iY_hgW zxir=R))B#JQ^w54Vsxwo;j;(7pynD&{^}6-NaoF2EnP{)GD$Nx$>?t%C7*_qwtj&0 zTDhx=bGoW^Q7<6MIw7uQxpvo8ti*y{&ef2bUL3`IvC3Sr#+~2KlSIVbf8&k{;{2J{ z+I7rzUVTRh>}Srn^Y#Dub2m$(ew}|-U&X$!a7|)-(8Of=c8*^bHn^#}mY#0c(Lu^{ zwbB1Ib;ylxdlWBndOAU=R8(*ytDYbbbQ>}>aN#bRsh0yIMzzI4>jfOT$Pmo#cNhr5 zg3>hni5my&IX7Radclz))c=K4(wu%7l}TfwTATlEkG5^OE3dbN3% z;0R|mSXXWB2<%KeubOh& zF!J#v(M6z7VW;-S(X`G^9%2<^7PuccR%}Ui@x~2&1Unq6(RWxlC=j)$c}n51Ge=u5?OU8@NNQ&@ z0Ma|NsK}=BEwe~XB?%nC4*oQV{IqDlMAa!G;py7~Lkb8O_A#`D#~8H;9e$O5D5q|L z!tZt42(1}A>Nx*#w3KdkQiJGrj7=>NJx0OpL@xKRQp7QB-@vl@O%<}{P08lPSkA~V zh8p=?95f=0l~T?bAk>N;E6;QZfWZva94j&kFg4hk58qMd8n_XenmqZC$k@s_!WT8` zxPoHy(JDs0FWFZG3Egh?1!wD_RGF)Yegxfx*>MOMyv<1*ZXAyr-;CEO!T?%;Z~3-n zzQDNK{Kre**}iBkmgz%X3cbA~>GUee+MG$itY??c{Zt`?W+vbcyHY)#=8O;lZ0CV! z5zmY75=I83B@O8%oW;JXs_H8dOw;oSSP~9Xd|=n)*%W@8Bg5J)QIY9g0tgKHzvd!X z4Hpx40^UNhnPa8eejQlryMOzeUM13Bape~uyLn{r^AQbE$4aUD=8l-`B2mZjn)dJE zhE9|!92!%5?srVMwH?+?xWD(HP}Ya+gWHQoD( zNT*|`(}5p;GaM?6J!UQ_ESu_OHD6D4DaJn1Z;=`VtcMV{S5Pdvt07uO3FablUc%Nc zjG<*6`cv@UXji2+(U^iEtJZRLxQ0|pO_!R{wibC=2L`Ju$X8$ZsCPWLbNdUp*SE|l ze{qjoE6&Ms3>Bp>@FAY5`wq=}W~-Ot1zrOLJ6{{WS9|M8q@ zR74{xV!E5)#in3XYZM^rD1@vPam0OwW*%)3Gk_3=x+ER>jv;>ioQYQ{wcVndd@rYv zGqz&AVk4S<_7icPe`H24JH*#|BTI)rccpPKUBVU1*)s`rKh|#Q&Mfwgt8-DoRy~$p z8?-IauwMna?Jg$PtvGg|kzT!BqKZz7!Gg5*w-83l+PUX%x>l#Nc^8^+hP;Zh*XS34 zmq!%Qm_B4(B}R4>Lewdw{q=f*mQh%=>1Jh)*a1n%ofazDQ2faH33WS$mSLNOSWQtn z|aooUH=9z}tvnp`vh+uDwykH?Kkj z!Dj`cj0x6j24-!Nuh!oJOOS)9CrWAkT%`@inudr`sW7bAsJ`2T0VCnlYE?7kOZlR+ z#7iNY?Q1D3-x!xdz&Xu)S-1E@Zgb-V(cB&ZeEW=GRug;uP>;QaacpdmGGql5)qW!M1nYA>byO0nSQy3vw>YHBkh6*PJ*!H z+WiBvFWQo{X(5LPYlq_%ABAkTa9T<)QNco(fme_timB4l7NJkAZO65oTLcQ#>Qy@DtPethEOF#D8Iu}k2PexNzH=WN5OByT zX(wx)+)N5F*d@E&VfbNc7UUs>nni$~a+FfZ;6}fCRC@~jEh&#c7#Y#vd7B08B&T@Dnd(XQkB+_MZtPe^%bX$^TfVyD zebGPSY2K`jrj0dusvQG*rKyWl+Wed@HSz3@*fm@#_G}4C&IEckj`#A)4)y#6`VNg_ zF0Rdb;N}T4${9%^WIE>3a2Uf1op`8uii{E$r8HD!HhMkMgxd1njrY;hke7vkc$yL7 z(KP%!fu_C;iacdk;G7=Q{SVN1Y(t_}>)<7t34bm4?MlhYW~IZv#$W7{;)6u1fTgFw zyLIk785hj_j}>P(&KJL_8g&7M9~BoVB5Xog0+YozOEL*MNaJ;?3E(XBl7;)*!!))9 zgVH-@(nRnd={TXD1P-^!`V`%$ORwSik_-G}GF{yxG+afz-k2@UoY!`ImhCl9k>usCEcj%Q@-cQ=1{VgDUl>moH)( zHU}NUVN3>M9S_m-akJNvZ{;1t$d8K+PJ~oW$@N!CdgoGIBKdE8qXc(dy;iqe+pwDI zkduDMT!v%b-;8KImsD6iqFxPK-9tP!`p4QIU&O8s`d{8VEOr~B(=YF`g;(;B&I;xj z7cZ~aIH}N3)PZ6Krwv#kd68NDFq1v5eT^$89Cgrg44h#H-#aOvw|G--IH`pl9UYTN z7auV369kcq^Q33ux69L)dG!KUK1-90_4ye1Ha#OvMCne8Ej3K2{i?A_kw|M0^}DvL z&kKb2mBY_JzteH4SK>7J*Il%BAC|Pw<}*Su<&$qd1@$b(8k4{8q=O8GF% zk7AFdwXpZ*6+3fH0#pK#`TE&oK>tl7`-L8QUhUg zABw&<&AC}|;92B4%wekvFq5^0I*@X#s73MM1{)!^5T*_h5QfBH=uT5`E7m2cmzo*W zI#qVmw$K5$*qZ;&lx-`du>Bov6tM4x)VR(n85VR)Rj}i?Ham$=0(CYU)(FC>4}9^8edzpB z1s(L*g+R=}cMJIBdzSc*!wESx7f>3tj!+>GL3qwAd6-WLsbiGAe~&4uZ(5fm?ntFl zZkLa$P@|EU%Hzy65P=Rmh}Q=HDBY9*k|t=obf&$E{STn~bB~Ou%gTi7ijY{Ftsw+` zkc|ORXDH3|Vsuye*+)pp1bc*n^Mg2+RzsuMit=4I*h0EIpG0;42j}iKpZLe|Zc`_p zA2k8&3Id0LU0D^GSnG5wzxxoeaJLrAl@vI76Zw**t|%(s-F|LI=>KSD^-k}@Y4Km5 z*e8t4`2uRBH9h`zyN7p1vxp)uOSJyZAG4px77i4PsF$T?2R`WS*J5a}kHxZ|dDZ(% zeQZe5hdrJgaMhjZ^b)HBJo2eGt3KhXUJHxP^wNK}JC2fyp5{0`?7_rtqv0MV)(khBL$lTI z`lYX|@J^-;4HSDwYg8#UtzEBQ-flP|eP>8Oi&AK9uBJkHCZ#a7818P~kaa3Y5<#5^ zbz^z`Om_YHj@Kryps|bKjS`E7og&i(iF#hCBOS-C}dmxL2~t@IyIa zLpiO%4lo{3Kv>gMPA&?6U^pW+DaE%tze&65a4EaU8eQ3bUCA9HxHa)Lh$YVk#D1Id zaS45Qe1s#;d4db3P&Q-ZA9D~=TX6S9>mDiN!GsTjGS`B8PZdpOJ641I(O#qWM(pF^ z8FW}{Y)myn+W?I`KR3OpLbE_}d59*7C~D>Dns@2Mt8bW@V`?#uKAMMJDc5j_nWizN z@E)gGVTgr~MaV&DjST-naQhBVlzPNF4e*(Smi!;Kv+NM;5rEP$F+J2PmWtxeri1%b>fy^|G`IH%5kcMPU$Vjz|O8g~QpUtCPg+X|3uX zD;4U_&(vN^k{QS8EAyGWXK!k^We=z=@Hsd0IC`oZC_14o#6Xo$eDqfplMrx#*PG(3 zE5gJrbM$xA(c61ayC@&3KCV?joz$0)USL^6%Xyb=O^Y@PN0bUIGinvRN73^9$dz|l zR83KtwsqLZFUfh&fANVRY5L1S%*x|=)+F`H@#AZW5ej!+5PzxtE!t_*?YhtNKqa#f z=T!ZbX0S1yDOp$==K9dSol;IVwwoWYH89MOBZAUSi&%SwFRHgx!Em)=pxt+kJ)o3l zRG(Jcc1hh-#a>*Vi5JW8B-LT{m?KodW~sAb@&r5Y#F(JOl3zp$k~eEw&!QrV4PMmh z>*a`ZE$bA#cSiuoPo-D-W#k+e2zDQkKt{KJQLA z_<_L9PyVe?@O1>Z8@|?VoeZhrUHF6FXr?aIcb%xPR&Y^vw62z}rQJPiUAP{nQo1a1 zO)qLjF*nxjETv|}@22$)StSJ~nsV|&=_%A!KYzU`ysggirALG!i?nezU_I0v9Yb%^ z{gR5?O1IreKy|G60{SkCo}}hSDyefXiE!O;$eM8ca|%n_Ja1AlS)%ec(jL>zi}l{K zFU$r+*>X(~UT41_7e)dB7M-}3=tKgf(?`UEd<~rKe11CU>zLM8dP5NGei5G=y)H*)usd!0c!1OisV{bA7U{G|d zd3w7nh25#TRZRZ0vQhP8cZ+jhpj)ijY2_Y^hKvZpKzBc}Ze`_n|F6;H40EYg&AHrA zihjt71y6Y>y9`c?S1PVvN03&UNx@nb=e4NT*&v8=a99wLa&6durqusi+E&wRV6K>{ zyv5+Db>cFfQ-U$DonK$Zo`j5o{~KMuj?Z!@8~>$vuT)()K{x<>wnl_NaO7?XqgX9U z{5)a)Xcu&v7*p)&)_hHVGV#sFZ(`;jKwDz+wR0ri!_s@n!1ty9CZB!uvEak2)=7p; zQt5-U*nQzrUFSB8-{=-`E>`*=ht_(iY4P6T#4dcl6_wbHcK zMGt70_iKTdd|~osRDzriv~0KNyIz)Y6Sd|Sr-AhAJbUr`S{#@3Cm1|n4lVw^RsQVUsJn6?k?%fKJ)d+d&@8m3c`+>C@0 zS^GHteqiN_9(Ut~2prgs_~#Q=Ug^38!iMdo3dgG@?mF*a-=I%ojW(*Ub4lJXTC0zH z@nh1)Cz4hcT+7qjLFk3!z*-gMG3{<4=h3IHyPsEoZZRnBrQs;^>^q-VAah{|uvw-O^= z>ZWGdaVo`HbtIuXzq7`i$#H+M=(ME|Xlb<9F1R4%G+9xJtA=_NDau zQfniHa4tpl^se}?a^UB}AiVhjv!D}?aPB0Vf;%ZWFklHA?#L`*an+c;qtWh&A@2~pR@m_rB)E&$3Dv++VnbgWe~JQ2QOMR7Hk${Q^Oz#PyXpv(nD=aAXoZQRH+hefvy9L(toX zWn_Ksh)oTvdc_HKBSrUVJ7<4VrT*tquZo$h+yLWr{fjtkoQPLEfF`mwnyzsjm6aYj zzv$|EzrLfnv+nwOU#LXs_t~+zesjgI7eJT5*s_-3QS!Ro*F*mF`Tj)uP+{HNTVDLH zo0+AcZtB2Od>|TA4N)DZTi(O@LowYJLjG`N^tpb89jng47nkF)KwzA?qfdMX zmY4kVvp-pa3Vq(+bR$mIIIGL2`w^X_r2+NdG2_+1<}h7G!-JQB}V9#DvSB z72p=S6saB7I()QOYiaOri}wal4H}<*I%)_fV8rS?&&gucTE&` z@(Nl5^v9EAPLJuLvxmV|OQ}+^61cW9Ny6#5B=~5leTi2B!I6@nbNEy~t>T*V>QfT! z8)GOVH;uz$k`w*KmZ9jt-x7bxK%6rF#?p*j-Dax2uOfuWMS5J(LbVG7Los|dVnMMoceEEt^Q1ybn$em^5p}(Zx=vRxMEa@YSq$xY8W#2RGbZL zy@b}R-uY?@9~v(-VZeW0%A9Dfh2huMI+XP=dlPeWKfV(YGvjew6_)Cfk7!?>o_f3S zpr3D~2Pr9oBeh~wuL{9y>2JXw&zC{7X-B_e8=2nIo2B~le)U<6+1FDXSMXfD;<7aT zwKlDEow160I0!d&9%H!8E;_!*$9XA15A3YBq-XSKTU$#r%Q1787~Ah45rhrX&QPC+ z1~3;HwO*ZK7!irA#f-6aRG#Vu)#eeNC?iwJyABX8M%suOof3D7_ja zFAvGTZaz1xVtQ>Pw}u{V_F!JmqyDzXI_G4E!?iVYj%@ex-$$Ui+RWdC&darK^d zZq{20YVKZBcBe8sjoae(M|0ST+whOkW~L$6@1}3Fmydv#V?{aGqMc{A&4X7`rkJ_p zUY4TiZb~K0@G{rB&rR0d&FR9; z`q72J+HJ!E9VX!{6$boBxqZKs(PXR*$k$!*#6I1jtOUg$1j)krGM)_335wQsFg<8jL$Z}e6BwtD9G$b$ z8RWI|3=(^4p{cD^To=Yko4F5);tz~I>otrxW=V5EOV7xQ>j!6}ubqs$L78 z7463xHKUHG;nlGb2B22>T8RpVvzJR4s)$E2;_BV$Hs8z1Aji7M+R9nqB5c(l=pTTi zwzm6ppsu3pj(_#=U?%Wg%Rc2XdN;_1PDWQ}$bBu9tjoo-x@HE+&$h1MQppT^&0~zN(S!5PxFUjxRD{Gh{7T(=vsPQ95YIok5pzh!m-hI~+ykK$PbG`o@JzGCNw~#+! zhqCg*;bxRX{nUn$>bKxE^e&~zGa$tweVI9~cB`T8`#*r(i}OYSbDdE1)n9i-HDH@wIv_*LZLh3_2Jc`GZsPx=brs+KL32?$zgWg zI{WZbP!LlLc<(d)H^R`?G*<(;;%Kru&jZGOA6@t9Mik(Y&uLUp-^W?gS)tl*K|&Gn zU#}l`C*msi<{4*eO4LsKB}0!DX8xe0@HsEcxr5&*xR zuH2K%JZH;CwoTNxaxrc#Q)W__(e}J=LZM4=;Nj=21Fa;%@}c2EoJbb8TxR`8>F+lS zIb!@agAyCPpRaP4$M62aqFuQD0dCnBU!qY|?|)}DWG$j%a&rdkyUbzm#P8qEo!n8` z-3Ff^CO8|`mm~sM=u<%#gxZsJRm%B+o{=@CuIR2uHIvJ}^18;9{SGr*$8e^bO=@Iy z>_Tl#4g>9OzA>ArXQKg;Ed$BnM(uY4xF#PCNbyV~*#~?;_RyJbw1M&j0ORX_YfU(t&68_jem!LH&Bh{hyc* z@t<2B!0_s?S$1AC2_q#sn#M#6^kt)WtQsoOmrmmqcd^AaVCuOgQ$gG0Cy`jA0NISw zhiWg4>%EVd8jI=}-wWOsU02;-;;>D}wRTl@hz3dXRwD&C5ep zg4Uu`)=W1LpZmt&`Q*+lD`nr}9-~X5``SNKL+z*Wx>M(U9_>L`{%)^J?(p1?zdaYH z10lkVLodKyvC@Tn&x=0vG|#JbH_W#~^DahPIsL9wc7t{y{X}zTPyjNLGU@L~sfZQ> z;&cSPI*9L@%2=bJP4Iv*Ds3=3ES?cH%6$ zu7|%+%SFQ_#tOMozlt&QkBs~JahPSSvLI*_xO%Z}&883?wuh-rddZ+t(0D=6I&l$5 z+}rdLuRCqTF#Eujp#NG^8iWsD|AsL3%Q` zTIq6K&nrD0dlGqSopm#`&>z&|%na*l$jx$uJ!5UW97^tVT;c;cL^;&8mItp6cMm3R zEaa6kGEz)7+s-zzIDq+=M!fxv@|`O$LLL4D#8f|q2@ysE$3a&KVX)OWxeE8$eD$%^ z@uPPNFD9jVVp74fQTbv<;p7fsWnto_cbYZ$ENH}S$Clc+%+)Tgz)TMJ(-ChQyHsYA z7RhBo$$x;kFB@K_=c=5Pj->g(?t;c*F0sVp3x9JK()ylX`R8Eef-a0A_EXmm@&OEw ziI+f&nJZuB;_f>`$-ayF*ga+vS*8Q)ZtrgIdyXkL=5pOpF=}zY^49_aYty7gQikRP z6LRPFu924ReBi^3v3MUAx&HqHjzDq0fj<+xGNwlD7zJ*%irdX@9q2(;7W7S(2++NQ z3|Sq0WpLKDO{QQnL)0cZ+ToR5qgV0!Qk&L|yH%jTFFHKD1_A(@mBrC?k0V<$lF~|S zw?rHgtRlj1T!h5JD+1^yr>jD!)SaDfY7+hgu_tv;57ND?-P;4?6v?Bsx;M#y*+w_|Ii?3fEu4?IC zAEWFlhTy02S;w)NL|=*P`a0;RE-v4a^1cf+lrLrwzRPZW(FJoSmlOm>@}xw2da2&> zZ|JSFymWZ6qZ2Bo>0+qtn^!(RUP$SzeV&GLub1lO+c&JhBQ={7kTG*NRDF@heUHrP zv$wc6_c&VE9bHU3OqNxRtC7*AUW5oXAY_IXH0r|u&d8|nOa2Ws(> zW9@TMzT4LMbX8M7LhI}{T=yqAbcaucaf>&*lNNDtqFnNh}QFLHZ2HE0AP_kaz`9SXjXx_S}g5cS$lFiTrfHH7ce1SMqN>^ zfub*qE9Ll%!Xv*0jdarY9xcaGvKqaF_n!I_o_BM)0y!aJqeBuUsFu`itI(@T?V66- zSXm~KSWZdB+7mRpHA2XRax}8>i%R0JR6Cz~27v4F`o6$`&v|`#VJD^r+~vNZ<+5TS zG_Mi#9cV(oIxlYnycCLlYUESWF~QbHwvoLuTiWclc}xOC03R_ z4Q1B0Ne`_qEgc?fb2HmUEluf_CdImT@XX`T#PLj7ReuB0+VTkV=S!7bSWp`a2Sm|> zX&XyCjdZR8=A}~v%JYQqc*2OPwc088>nQ~AXgZw5!r++=Q@i2jn(-xft zpHmc_vc%_lOKSpN{{TB5R)JQ1G6iKFbx_)h;i_|WZLWf@`eS#VQ(N;-LnHOGL&GO0 zWpCE}m)4xln<^#gd%QCo!qaU#C_Ad%w=P7MIFvG?k2F{+3LZlEjWR2%I3<^#R7yh< zlv|zL00KF8*qK|kCAJ>D8ew#~8wSf079w&>i2{3Wps$@VMr$6zwC2B~&71STO7nehYe_>yk8U|c z;u|r~rY=$C=DD8sqVG2x(G8kHH(;?WTGm;cReW%_WpZwK=#F>4J^uh7eq^Tj+H;6k zZ*6sM24QGa)~#={i~=bHs4BpXsoAO)B&w3Fx13T}6nbI&`+d;;jyG3Npi{nAVjPWW zLRy@;)SS9ZOtXveKSC_cZ$?r`P)f9ZJ0y-8hgaHljdnLbM&vr^ky!9Kwu~-S%-))= z>)>Rc5pgG`7SI-TZDo+Lo#mf*G`4p(ocE+kr8Yg&rS0w;auZD16hD;qSAB;JmUTHW zyBe*uDzxG3b;h*IQFJiBjDx)|lW>o}vC*zFLV~^q!RJtZcUP*Uv8gZ6 z{$nzuZz?(>Zbj?PO>4PO#fEgN2ciCbKVrAKxn{w$c5;0G06M~r(b{ALivzX=CnfZwVxM(BsuIt=M#J!OIbiH6=gM)KYpUXPj;5`d;E^>*l3t zIjvar$0BvdH>TQ!Q-61YaS22m{3{iVLa7q7vNecU3j?~{X$Jw+8rBC8bO|96NX&)k zdMJ3d02@d^2r!ynIS`gbIlVcZy-{1F)rKcKe=}j$pc9f8XN6`7)CCA8xXzR1wN6`= zhMg%kG-}G5Q4J%*KFMTIp#zG$w#q@$=FYies5yr|yK2O*EG}PPh&;gc56y3>d`dqq zu4b4$N4dNe-yVN*I^Cqbq1VLwsr{c<+1{hc+=ABnoH6)A?)Q#%TSa^O^Jny#c50)_ zpKE*;)y?<6O|CrNwza)MnVBYL@XhZ+VVS%QqRcD=acB7Q7KsB4m5p^KjmkCUkxgNe z{P(aUR9<_UZ#p*bam{d<%p-0Y)t6e>*|gX3clgD%SzP1NJnwxMtbO&So1-bsC2mB8 zS~ZfpMy|ALeP~)#PFcjXO3w7k(TyU+i*@M|mc=Zl4Mg9RKVys4Gq@!4lh}r*P?Sk8 zKHB{Cria3hE5dyzLCm)~I?P9p#^lkb&TU_3)->9EqgPtmDQ$Zo%^44%@46QsL!sOo z*J;AyQK{Leizc_R*e_&yW!ue0(Co`asSzwii+t_DspoE1isIm>{I{;NUgkex&EAY ziIEi@FEt~%x$mHOUW2gdE3%a5Y~$F}rXlD3r$b#dMfKkDTvsijI@g`o=7M04euIww z1Y~F37LB~M<=1jQf#V~V%_@odk0l44R`y0-lz|h13bw&)X3(vs44O?MAK#bu@xb+p54doH=;_2rh;L2RwH$fQH+apHnlp%{Vb848FeLAG85 ztSm(-wwdY|7MgBK+@UoVq>wkgLZPxk+kF9Fp$iQHWgBry`FEzda=WgH~Tk) z-h%o60QC=wRPy+wqo3;i+w=F(Up_fqF3Mhke~I^Y4VhTkext};cpde4W_gk6w+}n` zZS`OBAN8%Gmme4Xp7JTP`ZIC!UrOcIt#1}&=GIP&zb3OUkixGYk=?R1YwL&OB1&NXQj>)RPv}HDg zzUWMwx*^e_-%d9Y4( z3lxv1g0Qu!n^P9I4VG#ve0EJ6-;LHm9gzB;9;vl@N;O4vFnBMZw6i_3;poKSGl)Mo zuhi~M4nIcueUnFCRAQ+db=bUlY2?EfDev){U5t?}&aGC1CqsuzTCl-#Jr{U!={+OJ z>2WN@1lQ2;aQV2maiIN!&mAe}y(e1JS3Nn*=yCj3R~GZ$(&)dr`=PxDkn&5m`3in^ z)-Wq!F1+;gws{R*Y!57Qo_79{cyE&ojdkUvR{IYNS6pl`hQ>@pUWthOsD*OmTN6_wF_cw@12lCG*!>6Y`n5_tvLhHYlnR(@?Yq*_-L zzKc7ax)+}L$u`+3fLH(#yTAzMLe26eaPV_@D^4q&OT$W@cXBWk9X$ir_1Bk&0=3 zZm#UFT5*nnzbH(d4vKhXd%qKb3R6d*?278dwzozX_6?W|j>s4@-i;E}SoTd`Ssgax zPgl=tzcK6W`4mb|(&g5ctx;)Y*tudgGO$JDQJwglWKpRfrSfN|IW0P4fixZy-nKG6 zGGXaNAv35qE3?zCjP74W`Hi7QUYz?YQOG@C#%AgS_?Y_{W>Q+ZSq0+AO9w}Z9X=L! z6V%L{>x{?^&(j(@*R;e z5}|ZeGQ*zM~UMlW%8=Kyx88WriKb!h+b2yEn-zzs@jWj0)iJZy)>nz612K*Z^a1r;g#}_T5_!# zuIc0^YslMtTdL`#p4ZV!_9+3ENdoi`Ddt23b>z4p7 zuLw#~87IkqxSm4#VXE&@_&>Wi;$ww+C*|*`D>tx;t8iMuH@w?TW&%wVFsu#>1V+gG z#u=(d9g>+^$s@8onIEOG36&ZZ#cjsix+%M&nYpb_dx6g3dhtuF%EG~-_*7C;8M3=K zp86VyzN=&gA4S!k%_KTD*CxejZLL&WD0C47sbv~faU(O+E5-zCQcG@HzHtEwR@6NS z%zJs==`MoL=-pP0;}Px7a_9!iZ{{@0Q+YpbN^=N$k*O&KRyC8d?WlFFky@JCWwDzF zO>T}&?iXVhKi~ZQlcUkEOpiJ-ITCp=(AFJwV=}t1Ke2A-lO2)Ab6%9AQRCM)RDo8? z8ens{ACvZWM3yw~)Z;?!3Zl@@t-;Zu>FRHc$>W=Fog|D%eK(U(UcsMKe^?(4?^~H) zS#j=&L8p*jM|Y&38D+mi`IP?vn7V79t18 zT_s!QeVlc3>WW@7H)WUW!z|MBeLs&!(cJgZ+|Au%TUiy_Q^j$)BL|rGR<}fRr$cxA zmz%24x$L}=h|BYYqflnu9x)=`X6~ADY+l*win1!Br_Zk~s;1uNcchl}Z+i4N)M{Ng za{C4xB>Q>KQ+lG!eBDT#La!7PtGO$zIbBHma^~*e6pSxbxtLJl+9gN*?mn43uwW}UKVr6XA7SgmWS|xq|06c|K zB~eRAr7TP=ArPjQHrjR8bI3e}Ud7SGJtiwmvf51E_FZZR7W%9KfH7YgAK0|#nsUcj za-LmRB+3^p<@NZd&#yxK>-vAkYcAj7Zr$U13ns1g3(mZ@b;OD_A@KhCXKGc}cdPNY zHfuVZ9hgyueC71zXimF*+2!t`qLU{NdfnqMvqAJNZnxgTAvc+(feyeXZv=!mJxGy_ z`G`h|L~2k?Ya(Eg>&o>mi^^%fcHX_C9?GWTs%JVg(-8S{mDDuZu*^qvh3$ofjL#e9 zyDIDG;)vQ?9(qEq6pgM;s%2K~M{fcU7!sruX>}B=uS~WEG%JvcChRWc1d8&PSMwKZ zf!9{8Q*HBmaWqcM)mdXj&^Llj&e9y6NB|=3a2uo8OD@SX`s_Mk9;x$_}O~G<0 z25RiP=llMF%e$hUnBOnRXta84XuV(z_$o$%(B-vx(nW zac0$OXU1s;jLl7RMZHclCt~w=N@Z@ctnAv8ZyS$TmNxlMe{1w7J@gl&@!oi(d5*78 zWe}G_JfK2mE|g)}f_u-^ct+&Qs;h{k^V2;K9*F5G%c9L)iy%l}vH5*#B#ua`xwDZv z)0Mg1)@j^YUBUHTLZK<+p91P`R_eY-UO3#nzq!FjfZbLIM1~U^l{G8L)2K;hW*D5v z4NhTD=C!0IB{+c9?1D8S(L|9_6%{#}z_K~3MyyLPg=pXD(gM4=2D05A}vT#CCZmuBaR@3M(d9T~Q)g-=!tLX5G!#SM29AnjO9E{zt|JLMj* z7cZ_J4Ar?(S;7qOw z&Al(qy}l1rT6MLV%I-#59$J;GC>58@`R|CQG@m2xk)d3ILU22#=;*bryx`Zf=mADq zycTbLa?_G1w=wh`@k8X_(C;j-Ix#yGX)M$&k6GB^o!H)(`aGi9o?0{B)P>y!O>$$j zc8cMVxnxhs`%5xI7=P*VE9VJp^$0x3zgmI*_1|I^k10G=pKOQiW^*2+8k?xugQ`I19fzt%&--J=82 zs@;DJ&{(I%9~SnUrr~bGrF%Z4x2hhmoZ`0Saqlti?oNj0u9WWi zJ_KxXM4fc)i(&QJ^jZgey*NDLA7;|_YwBluv(fsNK|N2DdFX0N{{Yv_xmqmndJh}R zvsKRu?7XqqTz$*$rk27bydPK9T1u*OpuJ(r-Al;qMq6B4d%OfWqUv*SgF}V2V`^I_ zOnM7xP%@2*GEF8-z-qfhNrj1ANTQPM6u+n>X$;VEM+FJiUt0xNX;z$G)cVYvq((8!hlZMxR)< z^@-|yZN;q4&!5nC88}PAf1+=kop$|X69cgGCxRbApFJN!+H+>Xytj`*$unEQGkOu2 zEeMTf@RAr`n2@m(i=nGzXk;6*OJ)(NHf<1cA5&XQLD3lnPKeGT5V>0wRi{n0yA$&b zsW<1Ii|4I5Znel)<=0zpTa1yZ%?dQe)vRqmOVNQLF-MSr6FbX021F~-3v855h)r;g zb?Baa?D0CX#DLfwuGDrBIp*h1fTrBy_NEe`p8@ogOwR1pHfG07v6Bf^(B~ws>^kns z8Li4nX>768)=O=oYeLUy?E5;oOeLd6JBd{raKTDg)vc12y0FM2eLg^%9NOr)BXfgx zZ8FvEj+ZPlH!O|$Ph&KEj=t&3U0xKe(_N0v4enhIdMH%Z7k*<48D5jis~IB8tw^=M z9qkday|eTBF$fIy3(4+ugqABT`WMY7JjK);*F~*-YulXL&+Pg5e?U{Z_M?yyB8-|p zVbOhGJz`!QS=gR?(%4Rk58!pMu;tV>M`4qHOXnN<%bPkzquNy+wN&-|zZS+N@!Wms zr%!V4QZ8RV91CW%bT&UYRg0w%UpLCMQ?~7CWMSEO0YG!CKb*S9lD%J@pWoD4|jSPc@wiaubJI}SP}PbAE@cV&>Wa!y)Vvv7su^J&^<@H{&cgnSYABA#K3WHz>54%H}wf>e%4uu^ikR+%#0dTiI2w z8?`*NYU@o6MEuP4-{7;HiNy&-E<`4Wp|XiG#6ZCgB|=+dDJuxqZZXC?P2w;7WA!K-KeQ8`jGe~x3lu2XKIxd52M!iwu6}qq@ z7dVAT?PgV$MRvj?PpofUhtAy@&iXUmE*0I6MvAb;2gu|#yR%l)t6rwH(Xaz8lmr|? zL`^QuO&HOxM7G+EuFUOJJDz$Yo;v()uAf0CeBQ)*iM}d;#x?X8J`%u@Vs8WVBrPtj z(j-z@wb>SpN+Nw8bL1kzuPmXa-KH*qVALlv+~)J=L-V~_`YZ!<(}p)RR0%o$$gQkY zwem9~jP$uWYjbO=#Hd!c<<%>DWp%k@kg3#D%zD{-`zxi{;FkqQZl#W{&2GI8b96#s z559l|LG*rL!wEY!rD6!YSGP*c^3#iKLJ2;Car@m9D2z)!iSsG1H*|M7XS|+j=^kar z)S@Z*wF%Xhp_Ni)x2P#qQ(ZVKp0DO~9A8D*W^uXMSFdZ=8_PD14|z4a-S|$f=pQ`$ zHA|~QpP=*Y4sEWe0F8240CvX7q^c9c2*YsqtuU zaPZe|@%@LV{&ji{=eB3-8LU2O;C_obpFk`xQRA)s>wWJ#n%+on%NNwQ&27!-_oJRr zdbXp$pP~<*AE6BxjoBl|u;U9cA)C;o&F>s+VG%eH5~Uj(yGlmc6AXc+ZzYaN_2!X3 zu)WiKB|=qgQyYQjjtGyFG99kEbnZMTr;1#7oPguj;x+Q=e*vua*4gUb5>bQ zTpH-}=L2nqbOIq7=pU!;|&w00W zTcbOj*(6wq_F4wZt!zNgQK=+vrON|bn{rr<3e;HjN}d^A?nc;5>b~;+zINu=^L}U* z!BAY$oZQ-7HDhjw%c1vY2?qnyd35Tq?AjM^Lh(N3J3GThJ&`CB{sYJFbY!Y6Ci*YT zW_;bz+}}oo{I}G+nU|r=^o5f}cJn-LB!gB?YFV$O@LzVPh2j*LY z8@Ss%4zczsdZJ>uFywF zJY(tFjVuQ9d+v5+9WADgzIm!_{$+`{+pT!p)_HqZrTEvas(ZS|T_NsyK3inDSt;2} zNAIdajZxVIa!bPWJv8FnvsV*$^$CA*yD;WC1aH0P+jbq(c#F_v3S-kNN}1i7FQDbk z>y@^U^o2*F@mSylgm?q&eo=KVho&(c>;{2&T->uo4KP{MBahgc*>$y;9;R?Wx4-9* zIyJj23Uy>XTg?g#MyVxsL$*-T`!_?=CLEmisX&Vmv13!KLbi-3?D~Sx^38R24frdiDx0A-?y0bq#v^&*&5prc_rj$;I7Wa5m>JeouOO%ymX--VxC5z8p z6VEP>*2Ph9D|74XGs0L)Ta!(8P3{_A=|?Dola(m5158fT?AK&tM7;{Vw8r?$VnccF zq38~`AE~FX8UFw?u^Z*?qvg(rG?L3NZaJbwDgIaX1TD_3^^^f9+AeJ1KR`K?p@!>( z&S4s{HQQAUx~;b)iY>}*0e^G;KArgJ)aj!Wio{rDA7R!L^5+c^T@Grdxt%Q@F((8cUnl+NkV~$>ytbi@&#?qOaVINclGRTf|&QsUuf|4&X#igOhAHA}z(X5r)c(G^0uwprhER zXL)8v!IUcB8XX$!OBa)R&y#4=ct{wee169#c|q$JlK%i!_^em9xU08#UgGrQZ$-Gf zpMQ4DVY5#W^a<4Vzp#BzjJI$(=sei)IU$}&de`RBx6MDIkC?xwZ8^Md^&8E9K^~5i zG?OvwQeg)OGYb(02m&Bj98;|eD=D6Gn9R9Vc((*;ER12-S|k=dOq6?G(D)H1*D>o*r5qlG2&o+AFeObX0cAkdk3w z6`_Wxow3of$i$IiWP4Ga;hxll(N~2b*5h>i{sleec4OS3);x#OnsN6kUnbe0Z7|k;_Cp^vJ zRtc~KK5L`OjS#80J|WjvYysEiUl58%UHd!&tSjQKW3Ak%nfAG?3ZSY$s#`K0@px__6cu&zq$K1avZ zOP9-@i8>5kGtiPUNqg& z%;@$tFLzU&k2l0oxjbVBqMY?#Cv{Ey&p4?o28-7yJ+ekbZRxRs`sO~N^WEOZ zmcwSAQ_#VA+oAch>wgxjy)N>D)viCI>lw^x-+!KkMBUmv_oHs5dJ2T?@?*!Naq|A# zqg5nRx6Y~PUVG?XFK#Cjmc#r0dA|wPeFo^AD;i$0)<)3e&uvD!GCQdy8R(RqwBkz) z4#6d9JAlu$(7fUWp3}l!Nc^?EX1uLYDRfhb-JV%h#f%!JII}$uolNr7=ujxuA&M=B zBBasjtI)*EaY**bN2a=hPC{rd3s9|Sp^frBj9|zyTWA-cD`N6LTyi#jca*L%F64GO zC&>Q*UVMl8zr^Fcx5WLc#rD3L@P~KsejOc0Glk7O578#G>iT!95cJ%;c_(eBH8f2IJIug&1L`7GQ!HG7axSQ-aOHTQl1_M!6h* zNanhcF-Uagc79`hs`=Gc+0t?WCTqUj9rUoKZ5U**oytG^(6t&Av_4u7L zJb}+Sot9oj>%K2W*OH|?krEbRDPkCW#vi`|D0IFdbrDN`owlr3 zcbL?P=ut-AG;R)+=;qSpc2#=_l=JO@0G%5h^Z|5u%_O_U8p>`Z=ksB|()+W={{TRO zcEwp~)gD{a*pevxhkgpC>8I`rZ(M0+6hRM|LT=6y4hO07X;+f6i{1CVc%O8Ut=+38 z$ebc^3*+|h*&7Up=zlhx^2by1Hf;;J8>o28pV;L}=~Z#*IrGK!s=I2tHy2B*l;bz& zi3casb)0vb!_4Y+9Lt8?-maxx%p%o?s5|e*&s=c7pp`whPW3#m7sU~^&G!EQOmlBU zOx}LbT|LZRkIpnmT;VpXvbA(?uyfB%P<>tDev#3$!8!PkWen$w^P7IV@m_uP8_M5K zarHZ^b*fD~c}s4-3-XsgWJ4lwU}5Crj}AacG3PHR!zKam{L^%un6BFdjQ{{Xgldk04Pnn9;Le$O&_1MAn9zgPIBaoJVpOyunLmkatC!2Evq zKuuv&PoDiAS2~1Ey;qL8b2aEZ*6<;aa2eQ0`7Zhr`S|xAMb*uf3_9L-rKS>QO-c+- zXevw~OAgsW0K))uI6a7x8J`)_Hc1#{jw_iWwR$KE`ezz%kk->gdG$c0Y*O7BVzupy z2fBpQCc{$9W?s7<_v!-vqdJ^|FIu|PR_0~&BrhLFsTp}rLC@&+^_^GRJx(qJz>rRi z0>YWzS;S-|K?>HJQ>$%)x2LXJhp)xx`P3?H&Fr;uhgb71-fUSS#?Fol7^6t}KiJcz zQdv|WP#WF717g?vu{Xpe(A%hrTU8~X(ZWX*lUbR48+p-~I#+Y6Yn|Co9*hmo6UZFy zJDG(Y)8hk`y2s4~*TpX=EltHwranS?_?bFJrM0SGA?xg^JbF{n=2RCoLvQ8UsL~B6 zmIFG3-JfL`Ppa~RT-?5!-N0#HBit0y?$eW0P7^bP@%z0F^v>t>Uzynauhm?gk6<|) ztNH5}Q-URvuA1u8<^sJP*5*z?UTxw+PawQKg`~MSy)k0s#k7wGP?aoX^|Q*~QMmm- zMk^a03jA&iK>q+&4wch*QWj3GWt!2=$SSj^6LO(-UDbwiaLK>VZ3ksBbvu>OJj6f7wV2R>^={mVTxKU9+S*M z%$0?UkNYQtP zQR$*BRo-9!VQ`ftq>fT21f~#5M!?k_GBX_*zJV?n(_NQ<>gP%(Vp+;o(Xi%smZC+ z1L40$n=y2`#$RPb{GYCJo5H5vhs)f$(0kr&LDor!CfuJd9;kWa+PiJGVHRZ;_YpXhM%ph_ zwYx>wGFSJqn=4|Kp-ULfbLclKspOXa(p7CdqEO5E{{Yj8*1>H>hg+fBpp(k~0A5De ze6}HV5gR(Fv+Ro>qS7mJN@*Lakmq$T7bb^m@~tr$=+p$}3pSQH=C{=28{&7XoI-`=!;Va#L{NF$4d@9bC-r_n z<)~KCsrV*?#d~C|?+saX#Ne}NPZz(@ZVhgKM){QCssgf`qz%bbRc^(aFs#{f7zEo4 zpwuQ5a|&gQsnNeF5(J9M#Wbo5^y=(>mO~Kj=FLcrZkwNrXBxAxTel;Sn#BXYGJ#q9 zOd3{w10F*17J{LD+Zf?_+eV;wEx1ZmX(r@U0(BuQQX=bmrrK(Fa=#l1dfrNHEEi)L zL{;R-{zD-gV^T@VkAxW!Od4d73OA&&{I(6cKN}w!TJQ#N$nT9=1D@M)ZE-EU5gc_9 zp0rfq1wBO1P~gEbE-9%cp@`_oj-mk)TwGITNb)Wfc_1U(hC#UA#E=xkGV5$800XxnCTmg*8M7%8MXwd~5_B*+2S(}! zhz)^WXw106SM!PC7R8eMc^JxWz5-1#?yP1@&S5Tm2V zk- z+?&*S2j%fcjQXa)5PWC2Oxohvk7Pm*I)d^0{RaIXefkg0D^*tBxc!=(t8_5bqPBMR zRL0t(lByl0wQ986KV@rS*u#xotDcOcB-yEtTn0OAK=bOd8=a*+J~SlRcJt_nQBO&fwU+2eW=Lg$49C71Wma}Y`0o4& zeJ7L_2Sjf{!#s`Tp7!$o-9FLty`yY&yx&nRBUXgz5KL${NJw)!GTub!h%vB9Qa9r+ zn;evp&ahiDtkF{F>)RNXy{T5(2wV;&c!^NRHVjHRWi|s ztEMA5L$(crMFd&mzJyUq2q0Sy?X9M~u_4#wi$|xCTj;Iqczm{6ty@a_amndxhMLLH zy&ajJEXt_F=A-lM;tM-#Tw`($V(IcYqoim~n(1bS(Uw;~({FW7XY$OgZB%y^IpsZl zn=G%aKEEQTwrXp07X6GTvLS&_>*uABQ44)HlO@#yjT)pP&G_GHjiKSQCdh0ibqM16 z3-U!Dk^KwivQR56EKj8YC{7)#5IZwq78M19y%~T2!3|#pf@Q;tI!5>m{s)V7i#Uf( zweyo{Mk5om@)Gdj07#A8gy}}4O^Px?nycRu`hGLmsIQj+^xq%#K2hVL39H;`@6W#T zc0Psk#xCCE`;NTc8OZk_!R|tfJfP8Z3(--gU@;k|AsRbE7qJdP^lnI28Il7=hC;$_ zKvs^?1nkTZz3fDiJsBV{cmM^0FNB^*kQX-ReEQ`^=*<}l0ic99EHE1uz%L950l{E} zVVG1JGf;yCgyKu!S#r&dgi5lM79tx`p(SMTS6B9zHs2t7G;F&T76F1SsN zTUe%>v^vjCrMlF0WpQB=*PBFcDw;aFL~@pu>P|Ci+miz(Qxp`E)1dp3cql>A zOHr_xB7`eW491pLrcN>mGSS8nT6xA%IAq4Io-UD$46Kd{N}1= zyz6|^zEjuO)=V;M+TzqU^+|10q)G}Jf-AZGbA1gf+Fbn~lQnx|hJA8vnXen|QMS6Y zX4e(3ELay0(DVe3J0SWW&1OZI3dl5IEUN+`5CLm&>;?c$>v|Ak2ICp93QdfV5Ev}7 z3(o0V(>QFKcP(Bd+A_+!!@hFq_^+flUz%ezbAF+p{#zf`&! zUo$^ZK0PMBa+z|UO8f82sgXXkUCM7l;F_MJdgkJ*+0tIF_$4dn`mdqPP!C|79~h+H<|K(|Q(Vz8yfV}zS-im^UG zc8c64Y`w@Z>jVe}76pLq2am$6808ulEnas+p2|bQ6?6%eBU@5L<#Ak_AeLQUW*V-Y zXg;IJ3tGc9tSpKv=FjNg=PLVsxbz2=`k`~G!nacyh3XSQq@dC%w3mz9ST^FF9t>wX z)Nm7Mq;T3H2&ibqBC}C$MTfT?BKHK$SWdklFZ@&4_-Unrw;Xcgj&X>vJX11ETt0@+Z)$ifVLT-TfKVibvJ!uIIl73 z=s6r(Np=-`D0RIBFon$C+Aq&eouK+z`q_#0zU**$Z`1xV?an`9T|I?O)``_mayEJ2 zOp0UgGg*poBGecn#v6MmK#3X5%V{UZ3*_}K_qZCb zL*`~;hU^avxG(7w=Q~#dESY*0+*zaoXmT4qh@IM;-gd4Rqn+8{-L28_kUPR{Nngq8 z=C&EPf8-IS*XH`RvbVdn=j*o=YDzk?4_17Eb*G{9oDVBIyUl%~*Bwu+o`UJsC=o8e z(m|5%${?t3%}^!7&_L&vc*=SdB3U?fIJp)PoFR z$pGMv3H(wvZQQD@V~p6rs_HU-G$^c$G0k*2ay`ekWBCm#0pPD2y7c~rFK?Zh%jfNT z$m;1G0+7%~*zTYnb7$(5__p%9UryJ^)nbM0OdJeh6lnmOgpTNIA(B3u9X*l#L~&SY z6jlB#vF?<*>Aae6`uc4FwyTk$xwB19X_1q^oa|dZT8q%_5e|(GEy1V4LzOFt2qYIE zq%Id^>52^Xwy;R_;Tlv6Ae+=2YT_o{Gb>O6=7r3)Y(^B z)WcMzxrOh)T5coHj`7Jo`0)v_1ZGTRi339eh%5 ze!e+RqsHuuUFC;|)SS;Nn=;DmM`R9sKJmnwp6X~T`i8#J{fjZ^MOs&AV&IRJS0}F}dWFmRT`ND&e?ok`?{MDKLR`G}shj!F-qpWH&-s4UB4hR+ zK15N)SJ41Q6~9{8V~I*!be?oKAW1r&C#!7UZoIu0#AJkyG$+4?Rfj> z6B>a!&vr!|sZXF&zdf$6DfG^w6k>Vfe$>y8BxCHg6zrKN*`F+t1L_I|9n#`$L`8>M0K{zfq^9UHPi5DcRB*9>lKODP?@`O|tN?oFltB zwJ=wZ>);C9nGo(2==@aS&~>6bF=?%c(zxO6DP+Exw&^_jFSsl0Fr|RZ)W8jcjc0Z_ zP9%i80>ndZYTw)O9c^M3GHQxQ87jvYu*iCINLE%}i3M4yHzL&yN+EF*c;mYs*kah^ zT0O#Mx4$Rti7dccp%_fImbzN!rJA%v5MxzPRQ8=cPfdZ%fyXZHl45pYyB9^u;j+v5 z&Y~+RocYcj8|R~RV!c^K-JXxlEv2RN`*U@?3TX@C^rY7XD|YJ7VyaH7h2IJ_iEXZ$ zy(pX7yGAW-sYg~?RY5Guk)7*&I)_#Ves@m68f9Kf*)}%0H)PutfKOn&KSh4Ou*rUb z^ND(6(5kT9LRjXYDyTM;im#)@LyuaQYEzd3i17_kBi9 z!50?`+H^Q;>wk~$rYh5q(|O^kB=|Hl!*ynlN5u%YmMDjYu-Hr!fEk#|J%LGqG{DJ* zY)}Bx0|4`42S$ZSQqB-$dVtDT^ST~jI*rrC{TULMBkHx29>XIU59xuAqmrV6GLA%z z)>=*#^xu+B7e^gC{z>Vegw0!`VoP*1tON=YIf53G-3yVtlh6h@v&@W&I`_13?ar0hUzFO5K#iSkY!Cx|+J1yXg$wSet?s*|u~`3%e!Qx*}N$ zmT2ZZ8k==DF8OKPJaanzLh4%=sqwYXp?-OKpPkvFGMJVPFIRmdb?MJ%sYbJh-YC6+ zl9jvYtGcaDV=GUUB~NQpRB0e&kJ|Vam!LV}=ued0B7KJA9*3Iq?GwXacg&?M&{w=57!A3a+&CZzfe1S_ zidhd8nR;$j$rFCZ&Sv&QdsfdeYDZ}oxQkjZL@%C^YLZ7e{@Ds82r2C;bB>xCT>Al0 zcPifXbIxB+vUW~VnCu>~^(hos1ZW27wL~a&7HrlTyc4KY5NnLXg6b`gQfzo^8Xkip zlM66FDFL!=VQb^>^vb$K~&rHE>K{pD!ByKTfZA8T*b3L_F z9hJpBFKt#tikx&;yx*wj^?RtAeu^We+Gx{vbFOCEp^59bTWR*^s_hjPHL~TWq)gmi z<&YGMUptb>@psiMKvE@XG9e?X>%@@*9p|M|lriO)Kx0WXh^Hn!GO@rBG8}4CLkwy5 z8FtCqT5ib5_wM+e&h+cx^DD?V@iiN|G}Z|ucvhuiU0mpvdAH8K-5sBy^ZKXWcy4{Z zWwH8EQdISXz151k-A!mAXP7lbcWiIE^mayi%((b5kZ8@xi zlO4kJ7!ylnqAv;}_oG{m2oIetIdfDrUfd^Pdv5^Fy+E<;og6{FH`}2=xfXqKC>-(w z!Sr|Q)OqvYeC~XeQ40tVMqyzX2+@R$K@>8BY^*654W~C{)`5+CKO?Ne0-eD*{aHPS zURWxZC!C&*`G4=c66N)3I{H^y`l0@(wAvHeJgt5RM|zi+{V&ej%QU6x06W;cI^IUs zZ*05RMAc>)jDgs)D)}rl%dqqu-;tg=UNfVgxzcO1&vX?Q6(~4^od#(NSPuugS9=NWpc&ebA~jPg zVOVxx9Om=Y&&s^1BIvYhDi`UB2sZT*YLeMiN2Du%27nP4$rMo%UTCQthT%(4pk=(Z zLZTz14Cb@sx_pB4#*XFpeAP?W(QJK+#IsR)4=FEtjx6+kLE%$S?aZLovj|;@Oo2&d zYC)QbD@EqF*D(!a#wwnXtix+}Yks-H#LY_Eu%`;k_RydDnt zQq}M?f1wG1^WnG9(5<`xcYT?cUmQ4gQiV=74`U>?IW#?h6>e5->}KS)6RXsm*HWnM zsDZ9HLd1p)g$1m{i&M$6RFJ1a2;&wqOjl4KX>8sXf%$39Fl$vWa)Ff6##LP7%VpY- za4CDfb87QCVRt$`vtLpw4fIZqlk+R|ohm90MzO<-`|4qrU&y~h`|Ib8`uGN@?5Ye8 zP8>>Y%}Ft1;dE=hESiGA)sE#-omZdqVk<<0&OIs3y)L(%g>C67q_$~#FLN>xziHs6 zgu?~T=w3>YF7f`=19OvZeNhM$;swL>H`IZORqnohE>5JDLUTc2EeIqOAY!=55h&V7 zG*ny`wJn%8qGohs=X9B%^B86xjGo11wyy)cgKvyF zYUZ|YJ_}D5t|Q#{GvDOW8E!-?)$A05oq7w-eKFi=&au@hPIc-v$yQld6##8EU{pGR zNe&LF-E9kgS5@0>5ffds!B;jFu$f^^1lVb+QWUp$kJ5;jckQr0o zyJvOr-lEF3IeZtdp;ON+pY5;G;c-pLeHVxuT(sxbYfxYY6@?UWFAE6tRT_2a;-mtg zNJbr!6^3dQ@wr|}RYZCRY`m!$?KH-|IU}i|?6gpewY2t=DV;*#R{sE;@>=M7N`J@pfHJQqy=Jhh z(c;oh-SZsVD~81z7S6o3yzl9eU!aZ+jy`MC0W#M5tmclZy$ciMcbn5^hD;#Y_DpO_*2+}7IM1@U>$|{CunN3?voOIL zQ%7voJMmN6{{T5X7mf453xXeU;YbL;HQ;xlZf0$OwhgsrY&&|#vc(ib%@iJBnJ^im zb%D~p@F5I#$Po`0cRai5;Cdz7UTk_5O%qkj>-ql4!1OA$)U85ASJbTHOpxOTby$%L z%?u#XJcUJ=tXXRmT!vGDptWKlMnD`Ehz9m4FlFIUfMF1@hREZs2_S0?&ElKOmYWA7NG|7 zAl|{L5JcyDGMbC@e&pmv{>x>D$a71@tkDy;hYt&7QndoeqgI^Y6e&o1vFcMAVdtbh zf$LJ9%4y`83zM4xjPvNZ51;0A7km(HKQHl-#MaE|L>r81|DLbz|rBSzz+mWgDip zRb6KaZvI2-&Pwn3;#7rqj`nzI$1)}~^V+n$%ha*mcs_hvB(cAxe3du`TJ*D9(QM7( zT;8_IErfA4GR$hnpbW0}pq0Q2QS<=mY|I9QhG+qV-KNE|`Sjz*FHHLX05t4H?2h~o zrL1soU9N>n(K$TZ`rE4d0NBLOWeb=QlVTD90b-L27NiLW z1cb<3$Ra-#u@XA+M=yGb8_}Q01-_B;QI&OI`v$9Q%G0JS#c3kw+o2a#A$rn(H2OS! zvOv+v>oB)Jwx#T7p4)}5_Qy9O*_+W#UeR*g}Is7#<2duEn$GpKAH zO1P`C#ii}~>yy`sOE?#(_v63HhNtpAs%l(SMHfAkL$a1~EE73?(`s zO-0~nRD&*} zQ*98g@P{uA9w(*!_W#AnT6_D zqp^6--|2XNVyFBk+sR7h2f!q8q9=7fMs@8 z8b*PcA&C?cMusg(pdJV{p=5y2@(lNOe{)&<^JMb-H?;ena_s$2OjF!Lzn0rKt9|;e=cCh`%Ig`M+?pP4 zKe3SP?C>elyl?Pq7e_V8fLQ&wC(Cc6#-&uaBm zmFMpIJg-H_;;U8?*u+&s0jG*#b$<@<|HdFXa+l#_yHeGOA^y74&WmOfWjsJh!S zjW*GBHe%6fky~IkAy7ObWJFj+UsE<{L8canER&Jg<*a-Wh7F*e1&+zIxb_K;pT**g5^S?vT{-4PrJ7;;nQUM!|fY9*3 zZ!#870zQPehp*;*ic0F+>in4j7)H$uy~1V}kPkt_4Z^lm&6SWIUI2}RAX`KYlPoX+ z!uguivs=x{l9n^KhNuSgt8AK<@Rivo$HP|JZZ&7&+30ukJL7OZ!F?^ zS$XWLlscT=phcl5avQLU9d9=Vg`0@aM?bYXxmP#6&8hG?xxt>%mlj0746f=0uK z!{`X4h8AH8h}bMO}>zPb}8m*YIh?FMkR?(Einfa~cPPYhw~e zgabn&M-)~Ba)`Qe{SnbVZ}wML&R#}w^os6pN)uez+qvYWd`V?%Eviiw%iA{Zm&4VU z)(!c+YujICWjSiyQsEdQSs0`dymoARYR!5a8CT~ROB;~;+Av-s+xMG%!FKP{KuoC z^;`1Cp7g!jz5;J$^gBG+Wp(ugeonAjJ%VTP?=P>`XeS&Y;&l3gvExLZr;Y1{l|bJY z^Cl(181rkiimF;s?u0bDYOT4E(~Y_BxtppM%CESZX~ybw-DxRik!95$soE*gJ%q$z zSo%FVck-`Dr9DSRUNlFk@xu}!`R`7vX#fiCB?$`joH8le9ai2}aF5Z~l%AKxaG^_* z_c=uC0CW!v+;enZzz~WP0c;xsGcpi_!IGOxMpe5cw;8LQ$92DRJG}O9f7pEwIp!PJ zryh}AOcwr<*Dqq$6a??*;U^f?ATeP^0Knk}jIjV&`LYICfq+`U!JEJ`k469;7Cn)H z7T^F(E=`EDWB{{rYhpSOt^(4|F_}-9X#LA+d3)*R4vZ(yUqbx)>~PSaca3zo>~Ocn zYTgWl2P)ZZ2b2{hH3?|G2kNWX(&3?n#n(c|=YPgr6r8 z)i$M9^UtI;wz#+RI<0L^4Yg=(ZEpa2?fnt=)T%M#QI9zE;N*bXE$#oi45T0^V;81EMkhrO5SYxb;ET}cU{{TnIQp@L@ zY$+8t`Ey)*Ry@0WNC&NzmVJJ1wt6B2Z#py4l>~Amvh%RKkE!xfE9@E`?<=1Ak8f?3 zzmM9<657zKB{g9!QxIBXU~8>S5PXo5y;DBf*v zJtM?jEssxm$DgAf)91=eYa^Y}kZvwilL5kj8gsh*lPs;(38LJpXst5xf0Q19P#nJ3 zBR6|DYIl6sfjah@wz+)SX!XJOh3b4#-*DeuzcJ-8p4m?{V(?kl(;AbH*rB>#(A(Fc z%-~L!y7cl)^8+L|l8hZMaCv`sW_7F$Ke_p|#;7hWzn>**WLs@2HM3f@qB%T*IX;Y1 zShWG$(UC)f2?uLKlX-$5C690xA!v*Nm_P(DNr4cg24JKCm^gH98zQF!&H}_9a(W5p zh={#1^DEOPSofWti21*MQAHO;w{p<8C}2yg-L?md>-z$tJ`?DCAUeU>lg)c;am}S# zB;o1`vfS3h?vbDF%W^(bEH1qk!$~B1?xLtXva6&t!K^K06_(goo2n0z(A``pEaIyY z$hc9}T87Owt3*=bBCsqqihWpVmV#=~sBJ^(1qQQfi(0MuSJqh@9oqT5c2 z1}Niu+jd>-2p`)`jA$PlS(_3%B}Yb7!^X zvlV-(RIhB^4njSQyQw$=EQ8NZ8!J{amJ}M~3ZXD!+=e)|nPpE{oac(qLbZXW19CW; zgy_mtkfgyL0F+W9%nT=Xg;2dNVOC?S%MrlXPt_*U%fbGOkRx1CsLYFS@SQug?%u7~<5jhGyeu~P)m1;Y1 zw6>XQywSr=Mmj~to8c9|RkZXueBy6IWmnK<-uq>w*4$$vre9?V))4b05k`u#>+?gE z*yP3SUWNHTZ`#X3AEA7^bZy_vT;8=;8n)iohwPQug-Gz!zFIpV>yG#tpbkVoTTC{#^K4cN-hN^ zGn^T6K(~}qp}aC}Ygv%EX|RbGt7b5X)n{f`A0_(>8&ktCE4ISK=NzkI2aD=#8UdS0oFm2})DAPC|kxu}6M+@8~yEfz~LW$n_pSoT4{%F4+(# zJQ^q<%muU`nlaGf610H1Hn6ibo%6lY>A@ z$AAj4;!U6r(OV?_k+LD)Om&|h_z$Hb_P2<;i`97rK}H_84R0iMHzawFRXyasva2}V zuE}^yDEc4HP`e(A+`fHXH-t|gR^EnHZT(&xtqdixlEPz+98q$V3e^=1quohTUdS-y z%i3>a8@|W9D~9Z0Of17-D0|2@Ae=x<0Hofxl4ixCGk9QN;KD}7tc0Ts$N{}5kOvqb z_^h>Yjq_i=JpTa0((jKIy`l1^r|0i`Je&1h+A-m`x1T&j+YPmVQ(XSZtNI_m;+gqb zOb?yj@7|^IAJh(KT3ry7L~p@%oSfF7N#rO{wh=IdT}v$T88+*3)E-$|Ijan2J`+Tp zT!g47G8#~jl~AcMp@Kq$q#l@Qins)!rKwdRHZkWVC=($N-9SKi+4=)r=U$APc*6}nw`TY$ouI+g>v>;yFpHo{~iz{5-)UmGH z2y0N@#Gc*ZzTdl=Q!C3f#?;*AgC@4N_cQ|Lq%O^@?Wmj^?2V}$QYMD1bR(fNW7$Sy7%_l&d zVp3#YdwIkjG((XXq|)%AcVi?b7Njr`E7XcOP0E?_evy7=8Tsg2_73l2)P zs96G@*X*Q9q0(;8A@O5E-F(vtP)Kt|9R}J}iQcUiq^JmUY*x1j+)W{k%M(JiGLS%& zm8Ou`Ly-a^(&Xf!8l-D&odTNG{{SWW&6O$Sr@XqX`ugUPZOO}s?wB7pvrBWwX|QM( zLYKxviLIUKuP(+mXSNLcnpm7;*Rm#)nzXP{=?GIhkAr)%-f@jCt(9hLZFFy@=F~1I zlIF(u2IsK@bDL$Xi5lkRy(%S%2}qGrG-JIJ^P|yiyguFz%k^GERVAqziOHxQnykc} zv$E6xhIO@}T9YlQdc+G2N{_)Zh(K1$wHwDLj|M>06rq$y5U8Q5f;dE}BI2j0Ladb{ zLg5_4fq~Q>m?~BnoXDz#nLvhX3^1{SLTqL1jbH~R07V8jVAzUAh|?5Fq2dpIQH$kC zp0*hzYT^`!OCK_684cCxOBOZS?qMZ4$5!UX5n8L35?4&jIR2~8rM{X@F>CYML*1E& zyT%iqm(2R9=i3NW?Phu2BNhcYB0Vk0TObl2KDTC@9`Fuj?v0wW8M?P1WT5u%WTHe`a{WFi*MkjztBwpH&{ou#XXl2(P4YTFE+F^jJC z50iN#a@S1lzQ%Rf zUYnc2x}s;vMXPa{gN!pCTHMx;_}8um42?0@a==z;M*`Oq3>oY z{x4y{>A8uK@;}J$vobrg)3z-VkknmTUy(7nJhl@kZ<`twWC_vbpcuSba*(Kt5J z?WkN^me$7SW|y@aQn zAlp)!kj7AoR?Pfkkwcj-<;~+x<-KP};CQwymZk1aC!>PcA{UkS0m0(7u&^{yq<}Ga zQ0RrgNuw3%HW;G_6jMWxhMa*!fnRrxydg`{FobaFN<{`@l;<>4tpuRdC&#T=$PtSM znM;Lcqg?1>q78_lV`}#*wgIyU@<$@miVCXHCN@o)QG{WjFoEw3j2tsk0PTt}MYA9d z#B4eUw;&7w&;Z~Y4vj3?Fz}utzH{gloE zLcq7VqL3(D30rJ>+Q_bcPxY2Ym)4FKeT}i@vYicW?&y~0J7%(Pdi!C463<2CV{m?t zT}|AW(|y4?{pyyk=;nPi@C`ZY>ixMF^GWr&QqK=q`%QAwwwE1Z7V-DeMg}3M2@zK~oM! zt(5T=B2*cn2Qf)QS5SIlgkB_u2)IdwAe1B#3IU?jm_a2Nq!=+ZvWo;kAc9okAq;9W z(iOFN?(3(n^I0f0idQPHncO90O`_=GezdtN$?_i4yzGxT6nL+t`Y;YDo$blf8m1)L z^3TG|>7zNgEGwJM5HS^oAGFwFu4Cy=5x%Z}U84KGe_#OB)f_>#Rk7@2b`B|9L+#QLH#X6azIfvj%Yo!-o)b zgWiTn^9CD3!I7bsa3RpkfMyhIfinz{BEiU^K?}i;(KoDDPZp26qAnWvllau5o~Y`@ zJnhPJv%&P&(E8Hz^4{JxC5S4!AX+D}dOEPwl2*$pOa=HFwdcOPtX6OzFZxl;e6Ifh z6#7%=``vvHNm@{wzk<&th)DXpr`2s|>gK$NqTK>|a+zE(or1Bv!l5H#Qy@ZWSTwPu zZCi;Ai9^2uh`{e4V-zk8XNEC_gBHs!ys@jFlzmyPsoAfM-DPfbN1szVMd@p~y}8W+ zv#8N3N8j>Qoc1vc=Jb0kjxV@7JV_)EqszLUS#7$Y9Io|u7_?gH)l+q4AJ96v?5&QC z=o#Cb8>!OV)auB$v@|&}>WLaoYPEuZ#H57~kt-O8Nm-ZrW9C<;+PDX0z@O-!4}-H< z3qhw=*-`A`CAz~ov zK<0{o@<|z02|^hxx=!D(>deCs&EMwSTm;uvrij|2tiaPdSpClIyPAwY68#L|Y6Ud9 zffg{zoEuGZEO&wrMK*iYG;W;ttB*hBwDg0IFr95(Pr!D!G? zwJCa)nBJtra5zBMAlzl5Oml-&p-}3jLSqP2sXCVmRW&+5YNY16DN>zWxWfz7io{XE z93qbZ(SXvX9Gv*a2wIJy4@Md=n651gP;Xh14>!3pHMs;~V~}GkV5(k~d{)!Xaycvb z&B+$i%s;}O?DB&|RqCBR>h4Z}T-x2ztcuE%0!~Nel5_n7RbIM8t%1)f}OpU85@|^;9SM@Ap$trDbDo67p z01`x7AlNw#qab+3cHtj`A2f%Zv>zRMM5x0)plwJGLf4bp`{o}?K-lDqi|84fU0ZVM zZb;yPEp9~U-Y)5e^{KrFkcr17QT}gEGSek&$+fEQrJFS;eKzpr`P}m^a2}&-dbw)k zMEjF`F$1Cu+gO_%+Ol*ST3woDv6%D3OMPjO7LNMNLx|oD5XvyL;3X|~^zX|*Qf}d1 zXT3H3E8*@<#JH=orIRHo&5bjdFALZgtSZ#myUP^gl9_g8E=@-|JDk$FsdQ4(;>A&^ zS2Q&ACp2+60gYPGqEc09s7x_nhyw>>kpdTyHyjG#qB1hAr4%blBmy`MyI{HykTNVN zqU2IqKtx%#@C9FtW|dx3X>c9}@K(-B<4PHAC>xSOIq;^vEw$&7CbU6X>7Z{5QEIkpns-9Nq-d6V> zF=B*ddk-Gqa&0Y(%t@#hs6up#LQn&^8s2;=4Mc5-uxOODTw-(GS1U%XfySmV$AP0g zk-<_$X6>p@VS`$YLc`BaWK}t#$Rk6B9&uP4XCpX+&Lq(0Xd4jiX5`q(GC^jwAoLwT zd88c+gQH`_r8C5N>9M*+W8rc0Nj zUnQ0`i=(8zB(WIlt96m9sVY|IqAGbpS0Y0j%Fnp zMQa-kA!f-XmB45W1;}lpD(W_)0eUr*;~WoZEQdoHbXDNCpvdT3NLD$fZAzi_3LU_> zqy+|C2HaAo8`6ey#=!wFv?`u}t7UVkYl_zc6u?3dnQ{vCrcPqhS>_xCvLfLniJEw- zA2C_+&$@RGj63Xq4R<|}pD#vE-q78PWRXN&+YaUDFdd&r_}j2K539ylUiH@D9C8r| zti&eAU!(_u;A2vN$_ALJ927SmRbkEMlI-IjS8BblV_4zPXw7w6wY^bCRg6w0oR%@H zEa|ih1Dc@;!a17SvIEXUqD3JjaH$bTM&1O;48lhuoJw?Uo`}E{0UnuLLPnM?HL-jv zl}yKjWqKPdVAs6m=JUTtCk^_+<#$st_R!xmIzW z{S+-I-3zxfAx}ht(nIh`hQqD&<#~Fg?#AhFm++5T3O9J{A7O%Lj==UOWju0E?ADC& zg+H``hsG2>))q&J7xW@!DSa1`Y-)CG!?~Tc;j=cj z#BM0*xvdMMXte@Y1Y+i%ujO8;-N#-eP_Bn3p_}=_-01R4h;%zWcXN9^%MIoCpk>Kh zg|z?ju=YwVU zLd@yRPpgo@Bh%Kn*5KHZU9&B0?uR#+)w`TP+NCQR)ICm$8k8|{Si+%$s>Y=TFB%*X z#~XzUkHR3z(Ee~n z&dCbV*g*xUE=0wsC4)DdUYPmT*_nnwFXp)I399DpIO8cAU`V?WZUx&#sLlnGg&fRn z*$;{sj3!R5GUmngY-Ps8d)iYPD_-2B#Y>rn$o#p4_AiICoE_xWRRP`9vM3S6_LUGg z2@#Sh5+TC0*6~Q=jhh8-(CLEErPvEZga$annz-mN4MxBMPh`0Zt$xzZY!A z;JNBXa65uVfIMN0fqnsuc=e(g@$XQwh43UX?e9`Cmm(Aq{l01zBHy%Q7Ci(*I=H#4cL+nreTXYY9L zDD)4a@@;T_nvIJ&#k$*taeXWmrgGaxxLO&bV(5x?uOxNnOM%+E)_Xr8u@{95&@}+b z32dbC7jH!cd>WQX7;NAyVY00$N0vtw%<8SBQ-eaoTdL+Y4@=hC6tzx@SjPkL%xW5^ zqgm8Ky!Lb57md6!JQmty4xvC?FBsM+901xNR|%UXZh&jjmqe)uR&Wb&l~H5#5I97NP~z`j5`<9Yt*}pgM~w>IVgo>v@7}1*tW>!ZUglg5!?M;4TNHtUH&9 zy&>oCe9iCJ_>tg1VVfu*oDzc|$q`XQ$lYqdSIz;6J>0dV2$&UY8W79?S%;elC+GnL z!xMn2(54hC#NKHx${R#dK(VDmql1epgR_#>MMYgtY*Ogrb9tn@D`**y<7rbFejqiY zf$Zjj@-}k;dlNvzISHpwJL$Zl9EQ>f9mdjV4raF1@ltbqT0`jk!;z=B>%z~^-89Kn z-aQxU6hDRQy#D}n?st9Mo*L<^@_BZ9NgNJ0vzcU5D68yUOFm`AFv>Q4N`r>hd7oL@ z+JIO~Q93X%1A5eg4hTS$a2OSa4ni>OAe=u^17_&JZ+RxPEf`o$)CBO`MdK16 zUWW$ewx?FwG`6H#lDp6&aYtT;^vheNjr~c?T|Ls|>7iq8o@)bJ+kPGK_P6HD+M}so z5Vg;p56KPP4o{)%T%pmvhsVXC{U;s|&kfep8;9!Q{X5Uy1*@Cdc@{6;co}G6txrvk z7*fS)DJaWJlocLDW@J2T=p0t7cpRymEDe-mCsz~N_Q>zH<1ANoY=*i%39NQf>9*~} zkZqjyM}u3;m8n?JbdGq~Y*#}Yf)!XA&nmnP_(QOm%uwV@b_n1E^Q5vBoeyrNoFN1| zC+T^b%6@DczK>bjeCY@24U((=cU1ibt&_p;Mm<)WRu%ygifZO~4{RtmaSGnTXLVt) zIeH8aYUd})1n6MU8!3NfM1~8fJkq__p>;>r=5^7W7J+T_7hC~_JMUG596O*bLG+#pax|$;CbtDU;Q~=;Dk5AGEvQnv5JMkB(Y6+>vPR~V z&1#e-2R5{*SEj?&YHpAbM##f7^#wa1z?>$lQ-?+J!d=+AnWLXBzQ-0ZJ6?|uUh-^J zBlf-$8vg(zl2fiK@D%j)8{<-S_T!96SDjvvH>cQU?kVm+Be))-)DJJudHp+hv=$F@ z%tMa~Yp8KKjOVTNOk=XS5z&(cl2ujoDypg2TER4fe^$1ex6Kg9dRaxX+)mq$t4*d|aP#&r`*5U46jZ#{!FS6RSgIcZX#}?h!p!RG7(kE6~APZX5HYS1%EW0S-?o{he zYxytP>)=?enLA?Yeo^%|*!=IAV1AQEy=RH@w0)mY2}f>A8d1DRCr1ShTM_BPJh74O zQwgnHoYrL}9ak}~F8F&g#w(SvEHTtgV z7q9d#)P~NwTR66NSfXh!pA0Svp5Y@g>8`|&DV5z$3&LU2gu{NVxsdT#6y+3 z$F0jNHKoRlvBYoa@6D+UCG8$T6>$=~I`$*&&o0QK+O)JehA*QGo?loluKDy&5vAlt zlEwD$?_u%FEu3j2DuIcwm%W%PL=%@dZibQ3^3r6H#5*|B!yv_#v5T%rX=CoR+Sz4- zwE#9g>mwz$W~*R2*+8xUtz)RG)X8K5yCx-^D$LThFe}l)KO6woNzS1v3QNHnIMnWy z5sP8#5SSjqlGCaZ{{U0Yl<&#@s*5F(I*9|zB)KcG&5C4z?#@!O1S#Goo-Fgb)(CHf z`j?h1UaW%;Mu6|hs;{7YnxBOAp?78Ch)<$Q((f&qJysA;C=>eX8tw1`RqLL452X3U zS9=EE<+J2T%^beTxd#(N|?dH_IGAB-5aOpA0 z&xpD}3ui4gb@uGf%u;*kb!ntpx(I21FEsVIt_|7nN8_^wR8YF6KiieH+1gVd8yN&u}_yZ-?4l1ezV8a z-o|RJUNia~(Tv@G2Rs56kJ9+*$@SFg2bg@Js-1kX7P{U1H@f6@j=W8NUP?Q$YySXa z;iyg83<0C(V|tfyzxMkwbU-HA;&nP(Fo;}bMHSgj6kJSAc$30i=zI0he57qdbeT>Oc(-~eFq0>9l zO}V5C(ygOgYA2S9DN6Rzw4Yc0%t|;|$oldRh;f!x`U|S7v zfrk8c6GK|6wKJpzFVXUs3qL-=claCHmaC$hJXL`khc=9olo1x}UCELdl1e&K^w9Lw zZoQ1 zGcZD6!XiWcN+Xjvv^VVRk@b(_dFo79<`+#%S733zTu3=p9P4-Pm>` zMcd(imCRkIL|$Ic=kLmP-S2)=YB{kwVePp`9~ez;o_}}G1Nr{|L)()0Jk2j}qA!Jh zgU@)odBa`)MfV<_zd7pW+sd}v&j-~0JVcf?B!H&pR6&bMp-El4MZcPU+oq1}^C2`Ox=s?@~R`ATl{lNJgxo9leO{-bM% z?Veh5ABs0+=}B7{5ajK>AYeg9W%0SC6jro7i_I^93evVvHT{a%Ky1Xj!NPGQ}Xa5edB3 z#1{3nm@Q$O&29m2TQf6xCT4C;Z+nwiEg6}c6E&NYGcl%bG9B+ilUvM_Tg-{sY|Dy?5oYE+QrXj7QsHU#JT!aPm517&gmXL>tC7uc>&}Lf$8J$hl;E1-<)l! z#Vt9!Smnc_mMyEucFj@3^j5X+M06j@e&Y*0`n2_48stwFUG+bSU9Pc(e6Lq0jOq<` z3tx?H{zvBZJ5JK(E|2p5yiQ3Mdru9_JUi<*mi{~5-(3aA{W0P_{pRrtn|(Lrjn6Dn zZs(j#mE|hfky~<;8{gtm(yEIYl<1ETBw8^$u8%(@u8od~Qs!vnnUefc6OcI7q@vu| zpi1=E&1Dlp4l1o}0~$LhPbQ4I$e^b(YQ8>6q}`aq<8osXwmN__3k$yQ#Ju>_hP`aj zsnuz27FeZu&VGyIeFe{zq%r)=k}k~8&h5UTVIwDT)e~YW`cCNE(=O#n3M!q$B(&A) zGzf7uC@7-Es!d6`7Z#5|9%3DrT5EQquEaZqNd%QFRfz^2UeN9ZdQGDgVo=6cjk;HL zF`RK-i){&xqiQxP@`wov6gLP*BO5|%b<=L3`rW~6iSz*Au#IbZCbN5hIlO~L1W@mK z$iu*HLF_pIvqFbAy~&|wNwEiVZ+qHI&EOi|_D1t~MsGrsTg)>znI>;GX0$95GdCt? zVVRkdZ($TW*m6N^IS8{dOy0L9X7>npypvnZlX~EX6vHXd1b<-&OJd z09X9q>>6C&@jamHysE*;Bg5Fm!J|jphED`1j}~6lXxu z{)5VGOLO1Yzdl+TP^Sl_XNFuryPp7J=mlwPBHLUJUwpG0671(M=N^u1s_!T|+nDT+ zkA2^==hU5{-tz0DQRUxC`8xnzfupwzj68;KTB`de#_cbvlL+Er;7l!-b-E@^R5$uw=M(l-WsX@4bN z>C+e@Qx>}{Ww_30>v)^9ohiCIq4TMYe9Ft0JEJRl9A2ebWJ&4jsYFsrx1IvDG~r+& zE;OP@t4ZlgENKt`P`ySGuusu?Ak7!JcYxGkfEo^F$P|MKWyUv23rm-PqKs4onpav9 zf6#uA;D-695nhNNRnv9hqfINZjXVPW7$Hyw*r84&fdHAs&N}4`AFTY$i#WnGSC-lFFpqx^ozHDTKm6G)M6;OThz5uSE^@lm_A*_kw!Gmf$xQcW7$(X35nO;bp^l8YXt$FKWLjk8uX} zrcqfn@%KZ?ymH(|CD!>PtaT$NC)BtiYcD@>T12VSdV&cdH6BJ6UHSn;SD@m^47~K( zA#N4}CAdh-mIGlfT&ELag(f5!QwSLWjM{OF3{ZH_3)i!NsX;<4h zdS`tf+dnx@j9%}Tv!zP*mPXx1U72Ks1Qcvifz1mb60HMP^Ud8RemiEp%R)uYg$OW@|-ITixje5JhMohV0D$^)K zZgUsKBuega=Tnx*@(0vbQ1hl40$vt(1Egq+O_AG~)Y!Dboe^Uf#%-wwJt5*={{YG+ zT+G`RUpR6E2|2#K!MKFg^b91j>e^voEo})JPhQ2vl2justqa5qDmaN41-M?ymJ12k zu+;ckxG(`r&Cy^a>?k0ZGByeLP|9q)EI5nJ3pcQ86E&=mS_XtXCF4Mhy(6gE8Y*Bl zk%O2deQ#TeW)-#q&@~U(hS{~3ruU{?G3yyMBj(t&WA-tG2>_$pl0mi|Lj8#0ZA>$prn-sC;(-`h#o|zBHAKTxR9ilZY7dSbruPd zy~MX9tWiyoDQsv-1XX*Y>uQ8o{Ro=+c{^QN5jf#TTd5qm_(juJ0ExSd7vsMvpWiL!QTH5LHV3|JHXkoJKA|%~C*;u%w`d460 zDG3Y+WucLkL#76%Aw4__Hq;Vn$f&wFIUcuiOYzoQ>^G-(Y4{3}u&gy4r^dyvtQ zsRSZ-A=nK_BMQJz*&C4jBv6Y%3<5NhVKCI9klZ6AP&Av6@e#QUIVhx4iM0xyC)A4! zLTrjcncZJ^^Gj+uE+0^kvk9l8qQ;eO-X}7?sX0b=gjla}R}^Il8)HZi(fINKV+N|LD$$AD()j(2iCBuz8X4#7MKfZ9gvE4 zUVY2?Y}H|kimdc**!Lj6vuaglQWRNIn9CR144z(=KFhyb3kX&0y=UMWQ|>>So>!1j z?md%D{cL4r%Bj)p_zbE_r=??9SvAs~x?8SW;A;<2*K8K5D5SMoGmfkY)nOpWIA0?? z5ArV7=&>lMIObb$R)e}LOAog4VXVC2D#LK^Nz~YhENE`_w5D%3R)0-g*zG<>GB;Tz z()q^uBZJE1^P=7QHpzso>G<``WZGx9F(X=}nuNI#9Wv@#`OFAs*Ocji_ZaFpl1^zk zW9)h6cSyO3GgWOiUeyswVvnD>xz8S*^^dE`+AK4T`3|n7NnLB|6>I9gsw`86N?w=6 zMFQIMV@$+V&xU1n_@0J6WS_TcF0Wc@xcBv_8JEn?DCE?l@D2I!fk9%rMLm}sx58hA zg`{(XbK|TMDkY`*Kg_q%!WOiCgW4&J<~kw~>1@*6@GjGO32YaL{tjQSe+iJpX~t$mH*pzb!x*sQ_1pFqT6pJXE$tihSj{bOP+obQ_ol8&n=ycGK#gWoVF z0E7J$rYb2ZIkH6Nq%U-C*l?5ry0#TM6b=(5$Zox)*KgB;guw@|;S>??%r^B6--~Cc z5_&|NZdJgICzwMseA}aY)!63I%PFPlH!2uW`+{S*K|5z`84E-DoRRA72N1}pFX-wV z^3}^DCrt(q%k>Tu+dI(JZ=cCRL1AdB$aU8HU?cban~y_hWc$>w9))Wajc*;^2R)D6 zx}|G{?egc%OtbHm^mXUes<9`AyMh6$)wLeBOsj)q`3Dr@U{Rn@NOgf<;L?JtZDl}) z71H3NV;Gl(1;39i)~ytQQ(L|N_!7P@ijikW1H00I$-7Y2L-h!CJ~z_(=Jydkk=fE= z3c!3XyhjC;LZGSeH+e_?`yL19K+988ccFsFSKMzM!;S;~QknDqmzD52la6-J(C+q7 z8!epc>+x*viEe3WIFlD#KUn|t_rGrJQ{D*{dP>-`!5rc4TTjNAwl1)wjn0op#G*;` zQzvp)mX9S|2!t02OspgL7Tx6&8%R?Fx$}R7)OTh~^rkX)NqIUBibYbi&*t0>%?@5h z2NgR2p2fIj0L(7pX%is(Zz#xWF4q0AtP`+MtR@Y+yk(qH)evKFHW42bv$%)B!zE>9NpsTd}H@DKucAEPc;_ zAnfI@dm#a!@HLvYpF}_G$BevIn{4|>WXdQPo&t17624^LDlaF=NwIo&kr3(0l}GCo zMxrs8TNp25m0ZlLUnEY6Dp7Gp7Q#LA>h7|jpo1szLB%l_3DGuptZlzc71*PEpRtg+ zQv2SE=tK&!F0VJI0>j!zwztJGZ6{47*LVtCQ*lk6yVRSGHt5KYXXiP}6gU*iZCqi* z2MG$x4e^Rz8q3JKPOebT$edac@QKC;N`tXpvPKKS0c6mX4cWZI-;#>`-6w)c=Wk~J zu9to=peFHHBEcl!5{|*i2LQpeZ*n~2p12mg4yfpHQz;qN-v!#flgp+%{YpqYZ!%AC zxPdBj*`DY3ISM`^OeNaQ)uZ5lHOHO4(m8Bl?@n;_kr;iM=B0s#iM?`bNYD7hdcc-^ zAHu7qMs!t1O|3=@naZ8{CLUqQ&QKv_$j3UpFz2+G@LCz}K_pMOFU^I>nhHkhh~bO2 z$1;`}r?xXE5aV#X@}>C9QGJLgWu9EESYm-p={?2h(@BwG0A{bCAatNva| zY%(e?wj~VxvP9~-y0$G)v~E~PG>pP_?g|~Dj~1_Ib1$^+2GHib=vx_Aj#5Bj(YcCV zOx^|~N+!AZ7cnO{%;d`A3Zc}vPYD@?Fwb9d`IAwpKs$4SBQNW~-EE~yoK<|<{fbg( zjQ-{QE{Pq`Nj8ytS0Gk$!?dMK(!{~M7O%Cn)u3iup{pcw6Z?9#0}9y}YYv0M1!WK- zWU{EJ`h*f(U@GY3LDc6KbT(#1RCK=0v4+2ARWh6A=wuhyCCd2bb|`E8aOsL!W%Csu zmulX50H&!pp^rJ@TR_KWaCw_6M>5A6N~zVj!dHq4A}c6e5KZc-L!&}e3818ER;3P< z&an2z-;k6m0pWY|OWC|m7q;`M(kHZWCJ|f6++7v5MK8s#vF+vRugK8risH0Qx%BkYjR;49Z5g(;DQc959vg1 z$?jf2izl}>g_(ruqrEw!&b_#e6eJY(9PtlU>;C!lQ`E&9HLVTSY)l; zsO7sYI?P_VsXeG`krlQizNU4$*YsnaY`WjJ2MjqWtT`S$uCf0Cq}6|>ePr)63qpFe z1FLiJg|bL)FMEG=v~tKtUxGQVqxFBsH2p<>f3cQ|?3yv}oYPR%0*@W?IhX}ZU1>Jr zbr~7LJWNqgx64Q>-jT*Q;pPz-j+N6{K&SY-JH+sap=A3PG|dh11UMPQ%B4|OfHFAL z-B#%62ETntJ|J~M6VzDKQI}*7yaH2$1*qrR(8WZH>KZa-qXe8wSNAuO1?mSBr{#D` zDj|(rV>!6stOOY}e>Cima2I8Tsx{pW5YiuxnT#+6f3A(Tj0*d~k!jGWqwWaXQ~hrH zmU}@XTzk#To{W^K2Imv;qC_}@_wyYY<+n0eX|FOkalY^TXwrs+0jdnHva&S=^3;+ zhY~)=`(3I?e0hURgU_;Rcg0UwDsV0ycJMbWP2VPv0hbB zGZ&e>R{jIzBk^UG0M&h2<8O-T#YzRu-UuF6vfl0g0WO$}BYC#8FDW!n&D#F~@IWLK z<#3E0a(`K?S97$GDA*;4DIzY4nuWi5&)tGR2}J4I|1CYu#b!X=|CS1rF(WD37Z@G{ zSTraA8Xgp?A*{%~(DENf9R)KimCrXCI2Ijj*t=m?5DYB5S^R zc^`QpaYFgKq!y(L|MbEi=^}~uLCC~aTOJHPPVG&PJkuWtx1*j$Wr2GRIK;3=TftVXbMY
    C78=M%c^|gHjoE>&o=1c17J$gs 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�?r(Pt&y8G>1(=QUQ!o#@ecicqTp4 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∨DGIzqH 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 literal 0 HcmV?d00001 diff --git a/test/e2e/user/files/small.jpg b/test/e2e/user/files/small.jpg new file mode 100644 index 0000000000000000000000000000000000000000..eba421b87a79b5a33850b82cabc6c5e91b6d5ddf GIT binary patch literal 52721 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 { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + + const password = `@!${faker.name.firstName().toLowerCase()}${faker.name + .firstName() + .toUpperCase()}${faker.datatype.number({ min: 1, max: 99 })}`; + + let userData: Record; + let userExist: UserDoc; + + let accessToken: string; + let permissionToken: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesAdminModule, + RouterModule.register([ + { + path: '/admin', + module: RoutesAdminModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + + const role: RoleDoc = await roleService.findOneByName('user'); + + userData = { + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password: password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + username: faker.internet.userName(), + role: `${role._id}`, + }; + + const passwordHash = await authService.createPassword(password); + + userExist = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + const user = await userService.findOne( + { + email: 'superadmin@mail.com', + }, + { + join: true, + } + ); + const map = await userService.payloadSerialization(user); + const payload = await authService.createPayloadAccessToken(map, false); + + accessToken = await authService.createAccessToken(payload); + permissionToken = await authService.createPermissionToken({ + ...E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST, + _id: payload._id, + }); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ + _id: { $in: [userData._id, userExist._id] }, + }); + await userService.deleteMany({ username: 'test111' }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`GET ${E2E_USER_ADMIN_LIST_URL} List Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_USER_ADMIN_LIST_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`POST ${E2E_USER_ADMIN_CREATE_URL} Create, Error Request`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_CREATE_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + role: 'test_roles', + accessFor: 'test', + }); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`POST ${E2E_USER_ADMIN_CREATE_URL} Create, Role Not Found`, async () => { + const datauser = { + ...userData, + role: `${DatabaseDefaultUUID()}`, + password, + }; + + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_CREATE_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send(datauser); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_NOT_FOUND_ERROR + ); + }); + + it(`POST ${E2E_USER_ADMIN_CREATE_URL} Create, Username Exist`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_CREATE_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + ...userData, + username: userExist.username, + password, + }); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_USERNAME_EXISTS_ERROR + ); + }); + + it(`POST ${E2E_USER_ADMIN_CREATE_URL} Create, Email Exist`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_CREATE_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + ...userData, + email: userExist.email, + password, + }); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_EMAIL_EXIST_ERROR + ); + }); + + it(`POST ${E2E_USER_ADMIN_CREATE_URL} Create, Phone Number Exist`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_CREATE_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + ...userData, + mobileNumber: userExist.mobileNumber, + password, + }); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_MOBILE_NUMBER_EXIST_ERROR + ); + }); + + it(`POST ${E2E_USER_ADMIN_CREATE_URL} Create, Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_CREATE_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send(userData); + + userData = response.body.data; + + expect(response.status).toEqual(HttpStatus.CREATED); + expect(response.body.statusCode).toEqual(HttpStatus.CREATED); + }); + + it(`GET ${E2E_USER_ADMIN_GET_URL} Get Not Found`, async () => { + const response = await request(app.getHttpServer()) + .get( + E2E_USER_ADMIN_GET_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`GET ${E2E_USER_ADMIN_GET_URL} Get Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_USER_ADMIN_GET_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PUT ${E2E_USER_ADMIN_UPDATE_URL} Update, Error Request`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_USER_ADMIN_UPDATE_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + firstName: [], + lastName: 1231231, + }); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`PUT ${E2E_USER_ADMIN_UPDATE_URL} Update, not found`, async () => { + const response = await request(app.getHttpServer()) + .put( + E2E_USER_ADMIN_UPDATE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + }); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`PUT ${E2E_USER_ADMIN_UPDATE_URL} Update, success`, async () => { + const response = await request(app.getHttpServer()) + .put(E2E_USER_ADMIN_UPDATE_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken) + .send({ + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_USER_ADMIN_INACTIVE_URL} Inactive, Not Found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_USER_ADMIN_INACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_USER_ADMIN_INACTIVE_URL} Inactive, success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_ADMIN_INACTIVE_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_USER_ADMIN_INACTIVE_URL} Inactive, already inactive`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_ADMIN_INACTIVE_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_IS_ACTIVE_ERROR + ); + }); + + it(`PATCH ${E2E_USER_ADMIN_ACTIVE_URL} Active, Not Found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_USER_ADMIN_ACTIVE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_USER_ADMIN_ACTIVE_URL} Active, success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_ADMIN_ACTIVE_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`PATCH ${E2E_USER_ADMIN_ACTIVE_URL} Active, already active`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_ADMIN_ACTIVE_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_IS_ACTIVE_ERROR + ); + }); + + it(`PATCH ${E2E_USER_ADMIN_BLOCKED_URL} Blocked, Not Found`, async () => { + const response = await request(app.getHttpServer()) + .patch( + E2E_USER_ADMIN_BLOCKED_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_USER_ADMIN_BLOCKED_URL} Blocked, success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_ADMIN_BLOCKED_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`DELETE ${E2E_USER_ADMIN_DELETE_URL} Delete, Not Found`, async () => { + const response = await request(app.getHttpServer()) + .delete( + E2E_USER_ADMIN_DELETE_URL.replace( + ':_id', + `${DatabaseDefaultUUID()}` + ) + ) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`DELETE ${E2E_USER_ADMIN_DELETE_URL} Delete, success`, async () => { + const userBlocked = await userService.findOneByUsername( + userExist.username + ); + await userService.blocked(userBlocked); + + const response = await request(app.getHttpServer()) + .delete(E2E_USER_ADMIN_DELETE_URL.replace(':_id', userData._id)) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`POST ${E2E_USER_ADMIN_IMPORT_URL} Import Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_IMPORT_URL) + .attach('file', './test/e2e/user/files/import.csv') + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.CREATED); + expect(response.body.statusCode).toEqual(HttpStatus.CREATED); + }); + + it(`POST ${E2E_USER_ADMIN_EXPORT_URL} Export Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_ADMIN_EXPORT_URL) + .set('Authorization', `Bearer ${accessToken}`) + .set('x-permission-token', permissionToken); + + expect(response.status).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/user.change-password.e2e-spec.ts b/test/e2e/user/user.change-password.e2e-spec.ts new file mode 100644 index 0000000..6f87973 --- /dev/null +++ b/test/e2e/user/user.change-password.e2e-spec.ts @@ -0,0 +1,196 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { UserService } from 'src/modules/user/services/user.service'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; +import { E2E_USER_CHANGE_PASSWORD_URL } from './user.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { RoleModule } from 'src/modules/role/role.module'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; + +describe('E2E User Change Password', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + + const password = `aaAA@!123`; + const newPassword = `bbBB@!456`; + + let user: UserDoc; + + let accessToken: string; + let accessTokenNotFound: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + + const role: RoleDoc = await roleService.findOneByName('user'); + + + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + const userPopulate = await userService.findOneById(user._id, { + join: true, + }); + const map = await userService.payloadSerialization(userPopulate); + const payload = await authService.createPayloadAccessToken(map, false); + const payloadNotFound = { + ...payload, + _id: `${DatabaseDefaultUUID()}`, + }; + + accessToken = await authService.createAccessToken(payload); + accessTokenNotFound = await authService.createAccessToken( + payloadNotFound + ); + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ _id: user._id }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`PATCH ${E2E_USER_CHANGE_PASSWORD_URL} Error Request`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_CHANGE_PASSWORD_URL) + .send({ + oldPassword: '123123', + newPassword: '123', + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`PATCH ${E2E_USER_CHANGE_PASSWORD_URL} Not Found`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_CHANGE_PASSWORD_URL) + .send({ + oldPassword: password, + newPassword, + }) + .set('Authorization', `Bearer ${accessTokenNotFound}`); + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`PATCH ${E2E_USER_CHANGE_PASSWORD_URL} Old Password Not Match`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_CHANGE_PASSWORD_URL) + .send({ + oldPassword: 'as1231dAA@@!', + newPassword, + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_NOT_MATCH_ERROR + ); + }); + + it(`PATCH ${E2E_USER_CHANGE_PASSWORD_URL} Old Password Password Attempt Max`, async () => { + await userService.maxPasswordAttempt(user); + const response = await request(app.getHttpServer()) + .patch(E2E_USER_CHANGE_PASSWORD_URL) + .send({ + oldPassword: 'as1231dAA@@!', + newPassword, + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_ATTEMPT_MAX_ERROR + ); + + await userService.resetPasswordAttempt(user); + }); + + it(`PATCH ${E2E_USER_CHANGE_PASSWORD_URL} New Password must different with old password`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_CHANGE_PASSWORD_URL) + .send({ + oldPassword: password, + newPassword: password, + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_NEW_MUST_DIFFERENCE_ERROR + ); + }); + + it(`PATCH ${E2E_USER_CHANGE_PASSWORD_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .patch(E2E_USER_CHANGE_PASSWORD_URL) + .send({ + oldPassword: password, + newPassword, + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/user.constant.ts b/test/e2e/user/user.constant.ts new file mode 100644 index 0000000..dd5b46f --- /dev/null +++ b/test/e2e/user/user.constant.ts @@ -0,0 +1,39 @@ +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; + +export const E2E_USER_ADMIN_LIST_URL = '/admin/user/list'; +export const E2E_USER_ADMIN_GET_URL = '/admin/user/get/:_id'; +export const E2E_USER_ADMIN_ACTIVE_URL = '/admin/user/update/:_id/active'; +export const E2E_USER_ADMIN_INACTIVE_URL = '/admin/user/update/:_id/inactive'; +export const E2E_USER_ADMIN_CREATE_URL = '/admin/user/create'; +export const E2E_USER_ADMIN_UPDATE_URL = '/admin/user/update/:_id'; +export const E2E_USER_ADMIN_DELETE_URL = '/admin/user/delete/:_id'; +export const E2E_USER_ADMIN_IMPORT_URL = '/admin/user/import'; +export const E2E_USER_ADMIN_EXPORT_URL = '/admin/user/export'; +export const E2E_USER_ADMIN_BLOCKED_URL = '/admin/user/update/:_id/blocked'; + +export const E2E_USER_PROFILE_URL = '/user/profile'; +export const E2E_USER_PROFILE_UPLOAD_URL = '/user/profile/upload'; +export const E2E_USER_LOGIN_URL = '/user/login'; +export const E2E_USER_REFRESH_URL = '/user/refresh'; +export const E2E_USER_CHANGE_PASSWORD_URL = '/user/change-password'; +export const E2E_USER_INFO = '/user/info'; +export const E2E_USER_GRANT_PERMISSION = '/user/grant-permission'; + +export const E2E_USER_PUBLIC_SIGN_UP_URL = '/public/user/sign-up'; +export const E2E_USER_PUBLIC_DELETE_URL = '/public/user/delete'; + +export const E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST = { + role: '613ee8e5b2fdd012b94484cb', + accessFor: ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN, + phoneNumber: '628123123112', + email: 'test@kadence.com', + _id: '613ee8e5b2fdd012b94484ca', + rememberMe: false, + loginWith: 'EMAIL', + loginDate: '2021-9-13', +}; + +export const E2E_USER_PERMISSION_TOKEN_PAYLOAD_TEST = { + permissions: [], + _id: '613ee8e5b2fdd012b94484ca', +}; diff --git a/test/e2e/user/user.e2e-spec.ts b/test/e2e/user/user.e2e-spec.ts new file mode 100644 index 0000000..970128e --- /dev/null +++ b/test/e2e/user/user.e2e-spec.ts @@ -0,0 +1,164 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { + E2E_USER_PROFILE_UPLOAD_URL, + E2E_USER_PROFILE_URL, +} from './user.constant'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { UserService } from 'src/modules/user/services/user.service'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { ENUM_FILE_STATUS_CODE_ERROR } from 'src/common/file/constants/file.status-code.constant'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { RoleModule } from 'src/modules/role/role.module'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { UserDoc } from 'src/modules/user/repository/entities/user.entity'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; + +describe('E2E User', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + + let user: UserDoc; + + let accessToken: string; + let accessTokenNotFound: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + + const role: RoleDoc = await roleService.findOneByName('user'); + + const password = faker.internet.password(20, true, /[A-Za-z0-9]/); + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + const userPopulate = await userService.findOneById(user._id, { + join: true, + }); + + const map = await userService.payloadSerialization(userPopulate); + const payload = await authService.createPayloadAccessToken(map, false); + const payloadNotFound = { + ...payload, + _id: `${DatabaseDefaultUUID()}`, + }; + + accessToken = await authService.createAccessToken(payload); + accessTokenNotFound = await authService.createAccessToken( + payloadNotFound + ); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ _id: user._id }); + } catch (e) {} + + await app.close(); + }); + + it(`GET ${E2E_USER_PROFILE_URL} Profile Not Found`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_USER_PROFILE_URL) + .set('Authorization', `Bearer ${accessTokenNotFound}`); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`GET ${E2E_USER_PROFILE_URL} Profile`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_USER_PROFILE_URL) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`POST ${E2E_USER_PROFILE_UPLOAD_URL} Profile Upload Error Request`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PROFILE_UPLOAD_URL) + .attach('file', './test/e2e/user/files/test.txt') + .set('Authorization', `Bearer ${accessToken}`) + .set('Content-Type', 'multipart/form-data'); + + expect(response.status).toEqual(HttpStatus.UNSUPPORTED_MEDIA_TYPE); + expect(response.body.statusCode).toEqual( + ENUM_FILE_STATUS_CODE_ERROR.FILE_EXTENSION_ERROR + ); + }); + + it(`POST ${E2E_USER_PROFILE_UPLOAD_URL} Profile Upload Not Found`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PROFILE_UPLOAD_URL) + .attach('file', './test/e2e/user/files/test.txt') + .set('Authorization', `Bearer ${accessTokenNotFound}`) + .set('Content-Type', 'multipart/form-data'); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`POST ${E2E_USER_PROFILE_UPLOAD_URL} Profile Upload Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PROFILE_UPLOAD_URL) + .send() + .attach('file', './test/e2e/user/files/small.jpg') + .set('Authorization', `Bearer ${accessToken}`) + .set('Content-Type', 'multipart/form-data'); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/user.grant-permission.e2e-spec.ts b/test/e2e/user/user.grant-permission.e2e-spec.ts new file mode 100644 index 0000000..1a68f2f --- /dev/null +++ b/test/e2e/user/user.grant-permission.e2e-spec.ts @@ -0,0 +1,245 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { UserService } from 'src/modules/user/services/user.service'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; +import { E2E_USER_GRANT_PERMISSION } from './user.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { RoleModule } from 'src/modules/role/role.module'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { ENUM_PERMISSION_GROUP } from 'src/modules/permission/constants/permission.enum.constant'; + +describe('E2E User Grant Password', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + + const password = `aaAA@!123`; + + let user: UserDoc; + + let accessToken: string; + let accessTokenNotFound: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + + const role: RoleDoc = await roleService.findOneByName('user'); + + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + const userPopulate = await userService.findOneById(user._id, { + join: true, + }); + + const map = await userService.payloadSerialization(userPopulate); + const payload = await authService.createPayloadAccessToken(map, false); + const payloadNotFound = { + ...payload, + _id: `${DatabaseDefaultUUID()}`, + }; + + accessToken = await authService.createAccessToken(payload); + accessTokenNotFound = await authService.createAccessToken( + payloadNotFound + ); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ _id: user._id }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`POST ${E2E_USER_GRANT_PERMISSION} Error Request`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_GRANT_PERMISSION) + .send({ + scope: '123123', + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`POST ${E2E_USER_GRANT_PERMISSION} Not Found`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_GRANT_PERMISSION) + .send({ + scope: [ENUM_PERMISSION_GROUP.PERMISSION], + }) + .set('Authorization', `Bearer ${accessTokenNotFound}`); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`POST ${E2E_USER_GRANT_PERMISSION} Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_GRANT_PERMISSION) + .send({ + scope: [ENUM_PERMISSION_GROUP.PERMISSION], + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); + +describe('E2E User Grant Password Payload Encryption', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + + const password = `aaAA@!123`; + + let user: UserEntity; + + let accessToken: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'true'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + + const role: RoleDoc = await roleService.findOneByName('user'); + + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + const userPopulate = await userService.findOneById(user._id, { + join: true, + }); + + const map = await userService.payloadSerialization(userPopulate); + const payload = await authService.createPayloadAccessToken(map, false); + const payloadHashedAccessToken = await authService.encryptAccessToken( + payload + ); + + accessToken = await authService.createAccessToken( + payloadHashedAccessToken + ); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ _id: user._id }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`POST ${E2E_USER_GRANT_PERMISSION} Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_GRANT_PERMISSION) + .send({ + scope: [ENUM_PERMISSION_GROUP.PERMISSION], + }) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/user.info.e2e-spec.ts b/test/e2e/user/user.info.e2e-spec.ts new file mode 100644 index 0000000..ffb4790 --- /dev/null +++ b/test/e2e/user/user.info.e2e-spec.ts @@ -0,0 +1,63 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { RouterModule } from '@nestjs/core'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { useContainer } from 'class-validator'; +import { CommonModule } from 'src/common/common.module'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { RoutesModule } from 'src/router/routes/routes.module'; +import { + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + E2E_USER_INFO, +} from 'test/e2e/user/user.constant'; + +describe('E2E User', () => { + let app: INestApplication; + let authService: AuthService; + + let accessToken: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + authService = app.get(AuthService); + + const payload = await authService.createPayloadAccessToken( + E2E_USER_ACCESS_TOKEN_PAYLOAD_TEST, + false + ); + accessToken = await authService.createAccessToken(payload); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + await app.close(); + }); + + it(`GET ${E2E_USER_INFO} Success`, async () => { + const response = await request(app.getHttpServer()) + .get(E2E_USER_INFO) + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/user.login.e2e-spec.ts b/test/e2e/user/user.login.e2e-spec.ts new file mode 100644 index 0000000..1ce332f --- /dev/null +++ b/test/e2e/user/user.login.e2e-spec.ts @@ -0,0 +1,344 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { E2E_USER_LOGIN_URL } from './user.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; +import { UserService } from 'src/modules/user/services/user.service'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ENUM_AUTH_ACCESS_FOR_DEFAULT } from 'src/common/auth/constants/auth.enum.constant'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; +import { RoleModule } from 'src/modules/role/role.module'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +describe('E2E User Login', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + let helperDateService: HelperDateService; + + const password = `@!${faker.name.firstName().toLowerCase()}${faker.name + .firstName() + .toUpperCase()}${faker.datatype.number({ min: 1, max: 99 })}`; + + let user: UserDoc; + let role: RoleDoc; + const roleName = faker.random.alphaNumeric(5); + let passwordExpired: Date; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + helperDateService = app.get(HelperDateService); + + await roleService.create({ + name: roleName, + accessFor: ENUM_AUTH_ACCESS_FOR_DEFAULT.USER, + permissions: [], + }); + role = await roleService.findOneByName(roleName); + + passwordExpired = helperDateService.backwardInDays(5); + + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ _id: user._id }); + await roleService.deleteMany({ name: roleName }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Error Request`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: [1231], + password, + rememberMe: false, + }); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Not Found`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: faker.internet.userName(), + password, + rememberMe: false, + }); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Password Not Match`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password: 'Password@@1231', + rememberMe: false, + }); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_NOT_MATCH_ERROR + ); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Password Attempt Max`, async () => { + await userService.maxPasswordAttempt(user); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password: 'Password@@1231', + rememberMe: false, + }); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_ATTEMPT_MAX_ERROR + ); + + await userService.resetPasswordAttempt(user); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Blocked`, async () => { + await userService.blocked(user); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password, + rememberMe: false, + }); + + + await userService.unblocked(user); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_BLOCKED_ERROR + ); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Inactive`, async () => { + await userService.inactive(user); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password, + rememberMe: false, + }); + + await userService.active(user); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_INACTIVE_ERROR + ); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Role Inactive`, async () => { + await roleService.inactive(role); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password, + rememberMe: false, + }); + + await roleService.active(role); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_INACTIVE_ERROR + ); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password, + rememberMe: false, + }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Password Expired`, async () => { + await userService.updatePasswordExpired(user, passwordExpired); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password, + rememberMe: false, + }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_EXPIRED_ERROR + ); + }); +}); + +describe('E2E User Login Payload Encryption', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + + const password = `@!${faker.name.firstName().toLowerCase()}${faker.name + .firstName() + .toUpperCase()}${faker.datatype.number({ min: 1, max: 99 })}`; + + let user: UserEntity; + const roleName = faker.random.alphaNumeric(5); + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'true'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + + await roleService.create({ + name: roleName, + accessFor: ENUM_AUTH_ACCESS_FOR_DEFAULT.USER, + permissions: [], + }); + const role: RoleDoc = await roleService.findOneByName(roleName); + + + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + await app.init(); + }); + + it(`POST ${E2E_USER_LOGIN_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_LOGIN_URL) + .set('Content-Type', 'application/json') + .send({ + username: user.username, + password, + rememberMe: true, + }); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/user.public.e2e-spec.ts b/test/e2e/user/user.public.e2e-spec.ts new file mode 100644 index 0000000..6c190c4 --- /dev/null +++ b/test/e2e/user/user.public.e2e-spec.ts @@ -0,0 +1,171 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { UserService } from 'src/modules/user/services/user.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesPublicModule } from 'src/router/routes/routes.public.module'; +import { + E2E_USER_PUBLIC_DELETE_URL, + E2E_USER_PUBLIC_SIGN_UP_URL, +} from './user.constant'; +import { ENUM_REQUEST_STATUS_CODE_ERROR } from 'src/common/request/constants/request.status-code.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { AuthModule } from 'src/common/auth/auth.module'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; +import { AuthService } from 'src/common/auth/services/auth.service'; + +describe('E2E User Public', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + + const password = `@!aaAA@123`; + let userData: Record; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoutesPublicModule, + AuthModule, + RouterModule.register([ + { + path: '/public', + module: RoutesPublicModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + + userData = { + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password: password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + username: faker.internet.userName(), + }; + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ + email: userData.email, + mobileNumber: userData.mobileNumber, + }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`POST ${E2E_USER_PUBLIC_SIGN_UP_URL} Sign Up Error Request`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PUBLIC_SIGN_UP_URL) + .set('Content-Type', 'application/json') + .send({ + username: faker.name.firstName().toLowerCase(), + email: faker.name.firstName().toLowerCase(), + firstName: faker.name.firstName().toLowerCase(), + lastName: faker.name.lastName().toLowerCase(), + }); + + expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY); + expect(response.body.statusCode).toEqual( + ENUM_REQUEST_STATUS_CODE_ERROR.REQUEST_VALIDATION_ERROR + ); + }); + + it(`POST ${E2E_USER_PUBLIC_SIGN_UP_URL} Sign Up Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PUBLIC_SIGN_UP_URL) + .set('Content-Type', 'application/json') + .send(userData); + + expect(response.status).toEqual(HttpStatus.CREATED); + expect(response.body.statusCode).toEqual(HttpStatus.CREATED); + }); + + it(`POST ${E2E_USER_PUBLIC_SIGN_UP_URL} Sign Up Username Exist`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PUBLIC_SIGN_UP_URL) + .set('Content-Type', 'application/json') + .send({ + ...userData, + mobileNumber: faker.phone.number('62812#########'), + email: faker.internet.email(), + }); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_USERNAME_EXISTS_ERROR + ); + }); + + it(`POST ${E2E_USER_PUBLIC_SIGN_UP_URL} Sign Up Email Exist`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PUBLIC_SIGN_UP_URL) + .set('Content-Type', 'application/json') + .send({ + ...userData, + username: faker.internet.userName(), + mobileNumber: faker.phone.number('62812#########'), + }); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_EMAIL_EXIST_ERROR + ); + }); + + it(`POST ${E2E_USER_PUBLIC_SIGN_UP_URL} Sign Up Mobile Number Exist`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_PUBLIC_SIGN_UP_URL) + .set('Content-Type', 'application/json') + .send({ + ...userData, + username: faker.internet.userName(), + email: faker.internet.email(), + }); + + expect(response.status).toEqual(HttpStatus.CONFLICT); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_MOBILE_NUMBER_EXIST_ERROR + ); + }); + + it(`DELETE ${E2E_USER_PUBLIC_DELETE_URL} Success`, async () => { + const user = await userService.findOneByUsername( + userData.username, + { + join: true, + } + ); + const map = await userService.payloadSerialization(user); + const payload = await authService.createPayloadAccessToken(map, false); + const accessToken = await authService.createAccessToken(payload); + + const response = await request(app.getHttpServer()) + .delete(E2E_USER_PUBLIC_DELETE_URL) + .set('Content-Type', 'application/json') + .set('Authorization', `Bearer ${accessToken}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/e2e/user/user.refresh.e2e-spec.ts b/test/e2e/user/user.refresh.e2e-spec.ts new file mode 100644 index 0000000..f3a32c6 --- /dev/null +++ b/test/e2e/user/user.refresh.e2e-spec.ts @@ -0,0 +1,297 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { RouterModule } from '@nestjs/core'; +import { useContainer } from 'class-validator'; +import { UserService } from 'src/modules/user/services/user.service'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; +import { E2E_USER_REFRESH_URL } from './user.constant'; +import { ENUM_USER_STATUS_CODE_ERROR } from 'src/modules/user/constants/user.status-code.constant'; +import { RoleService } from 'src/modules/role/services/role.service'; +import { ENUM_ROLE_STATUS_CODE_ERROR } from 'src/modules/role/constants/role.status-code.constant'; +import { RoleModule } from 'src/modules/role/role.module'; +import { PermissionModule } from 'src/modules/permission/permission.module'; +import { + UserDoc, + UserEntity, +} from 'src/modules/user/repository/entities/user.entity'; +import { RoleDoc } from 'src/modules/role/repository/entities/role.entity'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { IUserDoc } from 'src/modules/user/interfaces/user.interface'; + +describe('E2E User Refresh', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + let helperDateService: HelperDateService; + + const password = `@!${faker.name.firstName().toLowerCase()}${faker.name + .firstName() + .toUpperCase()}${faker.datatype.number({ min: 1, max: 99 })}`; + + let user: UserDoc; + let role: RoleDoc; + let passwordExpired: Date; + let passwordExpiredForward: Date; + + let refreshToken: string; + let refreshTokenNotFound: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'false'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + helperDateService = app.get(HelperDateService); + + role = await roleService.findOneByName('user'); + + passwordExpired = helperDateService.backwardInDays(5); + passwordExpiredForward = helperDateService.forwardInDays(5); + + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + const userPopulate = await userService.findOneById(user._id, { + join: true, + }); + + const map = await userService.payloadSerialization(userPopulate); + const payload = await authService.createPayloadRefreshToken( + map._id, + false + ); + const payloadNotFound = { + ...payload, + _id: `${DatabaseDefaultUUID()}`, + }; + + refreshToken = await authService.createRefreshToken(payload, { + rememberMe: false, + notBeforeExpirationTime: '0', + }); + refreshTokenNotFound = await authService.createRefreshToken( + payloadNotFound, + { + rememberMe: false, + notBeforeExpirationTime: '0', + } + ); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + try { + await userService.deleteMany({ _id: user._id }); + } catch (err: any) { + console.error(err); + } + + await app.close(); + }); + + it(`POST ${E2E_USER_REFRESH_URL} Not Found`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_REFRESH_URL) + .set('Authorization', `Bearer ${refreshTokenNotFound}`); + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_NOT_FOUND_ERROR + ); + }); + + it(`POST ${E2E_USER_REFRESH_URL} Blocked`, async () => { + await userService.blocked(user); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_REFRESH_URL) + .set('Authorization', `Bearer ${refreshToken}`); + + await userService.unblocked(user); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_BLOCKED_ERROR + ); + }); + + it(`POST ${E2E_USER_REFRESH_URL} Inactive`, async () => { + await userService.inactive(user); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_REFRESH_URL) + .set('Authorization', `Bearer ${refreshToken}`); + + await userService.active(user); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_INACTIVE_ERROR + ); + }); + + it(`POST ${E2E_USER_REFRESH_URL} Role Inactive`, async () => { + await roleService.inactive(role); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_REFRESH_URL) + .set('Authorization', `Bearer ${refreshToken}`); + + await roleService.active(role); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_ROLE_STATUS_CODE_ERROR.ROLE_INACTIVE_ERROR + ); + }); + + it(`POST ${E2E_USER_REFRESH_URL} Password Expired`, async () => { + await userService.updatePasswordExpired(user, passwordExpired); + + const response = await request(app.getHttpServer()) + .post(E2E_USER_REFRESH_URL) + .set('Authorization', `Bearer ${refreshToken}`); + + await userService.updatePasswordExpired(user, passwordExpiredForward); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body.statusCode).toEqual( + ENUM_USER_STATUS_CODE_ERROR.USER_PASSWORD_EXPIRED_ERROR + ); + }); + + it(`POST ${E2E_USER_REFRESH_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_REFRESH_URL) + .set('Authorization', `Bearer ${refreshToken}`); + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); + +describe('E2E User Refresh Payload Encryption', () => { + let app: INestApplication; + let userService: UserService; + let authService: AuthService; + let roleService: RoleService; + + const password = `@!${faker.name.firstName().toLowerCase()}${faker.name + .firstName() + .toUpperCase()}${faker.datatype.number({ min: 1, max: 99 })}`; + + let user: UserEntity; + + let refreshToken: string; + + beforeAll(async () => { + process.env.AUTH_JWT_PAYLOAD_ENCRYPT = 'true'; + + const modRef = await Test.createTestingModule({ + imports: [ + CommonModule, + RoleModule, + PermissionModule, + RoutesModule, + RouterModule.register([ + { + path: '/', + module: RoutesModule, + }, + ]), + ], + }).compile(); + + app = modRef.createNestApplication(); + useContainer(app.select(CommonModule), { fallbackOnErrors: true }); + userService = app.get(UserService); + authService = app.get(AuthService); + roleService = app.get(RoleService); + + const role: RoleDoc = await roleService.findOneByName('user'); + + const passwordHash = await authService.createPassword(password); + + user = await userService.create( + { + username: faker.internet.userName(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + password, + email: faker.internet.email(), + mobileNumber: faker.phone.number('62812#########'), + role: `${role._id}`, + }, + passwordHash + ); + + const userPopulate = await userService.findOneById(user._id, { + join: true, + }); + + const map = await userService.payloadSerialization(userPopulate); + const payload = await authService.createPayloadRefreshToken( + map._id, + false + ); + + const payloadHashedRefreshToken: string = + await authService.encryptRefreshToken(payload); + + refreshToken = await authService.createRefreshToken( + payloadHashedRefreshToken, + { + rememberMe: true, + notBeforeExpirationTime: '0', + } + ); + + await app.init(); + }); + + it(`POST ${E2E_USER_REFRESH_URL} Success`, async () => { + const response = await request(app.getHttpServer()) + .post(E2E_USER_REFRESH_URL) + .set('Authorization', `Bearer ${refreshToken}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/integration/aws/aws.s3.constant.ts b/test/integration/aws/aws.s3.constant.ts new file mode 100644 index 0000000..a1c803a --- /dev/null +++ b/test/integration/aws/aws.s3.constant.ts @@ -0,0 +1 @@ +export const INTEGRATION_AWS_URL = '/health/aws'; diff --git a/test/integration/aws/aws.s3.integration.spec.ts b/test/integration/aws/aws.s3.integration.spec.ts new file mode 100644 index 0000000..d6fba0f --- /dev/null +++ b/test/integration/aws/aws.s3.integration.spec.ts @@ -0,0 +1,36 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { INTEGRATION_AWS_URL } from './aws.s3.constant'; +import request from 'supertest'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; + +describe('Aws S3 Integration', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [CommonModule, RoutesModule], + controllers: [], + }).compile(); + + app = moduleRef.createNestApplication(); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + await app.close(); + }); + + it(`GET ${INTEGRATION_AWS_URL} Success`, async () => { + const response = await request(app.getHttpServer()).get( + INTEGRATION_AWS_URL + ); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/integration/database/database.constant.ts b/test/integration/database/database.constant.ts new file mode 100644 index 0000000..cb35f64 --- /dev/null +++ b/test/integration/database/database.constant.ts @@ -0,0 +1 @@ +export const INTEGRATION_DATABASE_URL = '/health/database'; diff --git a/test/integration/database/database.integration.spec.ts b/test/integration/database/database.integration.spec.ts new file mode 100644 index 0000000..81c1f11 --- /dev/null +++ b/test/integration/database/database.integration.spec.ts @@ -0,0 +1,36 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { INTEGRATION_DATABASE_URL } from './database.constant'; +import { CommonModule } from 'src/common/common.module'; +import { RoutesModule } from 'src/router/routes/routes.module'; + +describe('Database Integration', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [CommonModule, RoutesModule], + controllers: [], + }).compile(); + + app = moduleRef.createNestApplication(); + + await app.init(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + + await app.close(); + }); + + it(`GET ${INTEGRATION_DATABASE_URL} Success`, async () => { + const response = await request(app.getHttpServer()).get( + INTEGRATION_DATABASE_URL + ); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body.statusCode).toEqual(HttpStatus.OK); + }); +}); diff --git a/test/integration/jest.json b/test/integration/jest.json new file mode 100644 index 0000000..3b00338 --- /dev/null +++ b/test/integration/jest.json @@ -0,0 +1,32 @@ +{ + "testTimeout": 10000, + "rootDir": "../../", + "modulePaths": [ + "." + ], + "testEnvironment": "node", + "testMatch": [ + "/test/integration/**/*.spec.ts" + ], + "collectCoverage": true, + "coverageDirectory": "coverage-integration", + "collectCoverageFrom": [ + "./integration" + ], + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": 100 + } + }, + "moduleFileExtensions": [ + "js", + "ts", + "json" + ], + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/test/unit/api-key/api-key.service.spec.ts b/test/unit/api-key/api-key.service.spec.ts new file mode 100644 index 0000000..c778224 --- /dev/null +++ b/test/unit/api-key/api-key.service.spec.ts @@ -0,0 +1,503 @@ +import { Test } from '@nestjs/testing'; +import { faker } from '@faker-js/faker'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { ConfigModule } from '@nestjs/config'; +import configs from 'src/configs'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { MongooseModule } from '@nestjs/mongoose'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { DatabaseOptionsModule } from 'src/common/database/database.options.module'; +import { DatabaseOptionsService } from 'src/common/database/services/database.options.service'; +import { HelperHashService } from 'src/common/helper/services/helper.hash.service'; +import { ApiKeyModule } from 'src/common/api-key/api-key.module'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { + ApiKeyDoc, + ApiKeyEntity, +} from 'src/common/api-key/repository/entities/api-key.entity'; +import { IApiKeyCreated } from 'src/common/api-key/interfaces/api-key.interface'; + +describe('ApiKeyService', () => { + const apiKeyName1: string = faker.random.alphaNumeric(15); + const apiKeyName2: string = faker.random.alphaNumeric(15); + const apiKeyName3: string = faker.random.alphaNumeric(15); + let apiKeyService: ApiKeyService; + let helperDateService: HelperDateService; + let helperHashService: HelperHashService; + let apiKeyCreated: IApiKeyCreated; + let apiKey: ApiKeyDoc; + let startDate: Date; + let endDate: Date; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + MongooseModule.forRootAsync({ + connectionName: DATABASE_CONNECTION_NAME, + imports: [DatabaseOptionsModule], + inject: [DatabaseOptionsService], + useFactory: ( + databaseOptionsService: DatabaseOptionsService + ) => databaseOptionsService.createOptions(), + }), + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ApiKeyModule, + ], + }).compile(); + + apiKeyService = moduleRef.get(ApiKeyService); + helperDateService = moduleRef.get(HelperDateService); + helperHashService = moduleRef.get(HelperHashService); + + apiKeyCreated = await apiKeyService.create({ + name: apiKeyName1, + description: faker.random.alphaNumeric(20), + }); + + apiKey = await apiKeyService.findOneById(apiKeyCreated.doc._id); + + startDate = helperDateService.backwardInDays(1); + endDate = helperDateService.forwardInDays(20); + }); + + afterEach(async () => { + jest.clearAllMocks(); + + try { + await apiKeyService.deleteMany({ + _id: apiKeyCreated.doc._id, + }); + await apiKeyService.deleteMany({ + name: { $in: [apiKeyName1, apiKeyName2, apiKeyName3] }, + }); + } catch (err: any) { + console.error(err); + } + }); + + it('should be defined', () => { + expect(apiKeyService).toBeDefined(); + }); + + describe('findAll', () => { + it('should return the array of apikeys', async () => { + const result: ApiKeyEntity[] = await apiKeyService.findAll( + { name: apiKeyName1 }, + { + paging: { limit: 1, offset: 0 }, + order: { name: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC }, + } + ); + + jest.spyOn(apiKeyService, 'findAll').mockReturnValueOnce( + result as any + ); + + const newApiKey: ApiKeyEntity = { + name: apiKeyCreated.doc.name, + description: apiKeyCreated.doc.description, + key: apiKeyCreated.doc.key, + hash: apiKeyCreated.doc.hash, + isActive: apiKeyCreated.doc.isActive, + _id: apiKeyCreated.doc._id, + createdAt: apiKeyCreated.doc.createdAt, + updatedAt: apiKeyCreated.doc.updatedAt, + }; + + expect(result).toBeTruthy(); + expect(result.length).toBe(1); + expect(result[0]).toEqual(newApiKey); + expect(result[0]._id).toBe(apiKeyCreated.doc._id); + expect(result[0].key).toBe(apiKeyCreated.doc.key); + }); + }); + + describe('findOneById', () => { + it('should return a found apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOneById( + apiKeyCreated.doc._id + ); + + jest.spyOn(apiKeyService, 'findOneById').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKeyCreated.doc._id); + expect(result.key).toBe(apiKeyCreated.doc.key); + }); + + it('should not return a apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOneById( + faker.datatype.uuid() + ); + + jest.spyOn(apiKeyService, 'findOneById').mockReturnValueOnce(null); + + expect(result).toBeNull(); + }); + }); + + describe('findOne', () => { + it('should return a found apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOne({ + _id: apiKeyCreated.doc._id, + }); + + jest.spyOn(apiKeyService, 'findOne').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKeyCreated.doc._id); + expect(result.key).toBe(apiKeyCreated.doc.key); + }); + + it('should not return a apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOne({ + _id: faker.datatype.uuid(), + }); + + jest.spyOn(apiKeyService, 'findOne').mockReturnValueOnce(null); + + expect(result).toBeNull(); + }); + }); + + describe('findOneByKey', () => { + it('should return a found apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOneByKey( + apiKeyCreated.doc.key + ); + jest.spyOn(apiKeyService, 'findOneByKey').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKeyCreated.doc._id); + expect(result.key).toBe(apiKeyCreated.doc.key); + }); + + it('should not return a apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOneByKey( + '123123123' + ); + + jest.spyOn(apiKeyService, 'findOneByKey').mockReturnValueOnce(null); + + expect(result).toBeFalsy(); + expect(result).toBeNull(); + }); + }); + + describe('findOneByActiveKey', () => { + it('should return a found apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOneByActiveKey( + apiKeyCreated.doc.key + ); + jest.spyOn(apiKeyService, 'findOneByActiveKey').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKeyCreated.doc._id); + expect(result.key).toBe(apiKeyCreated.doc.key); + }); + + it('should not return a apikey', async () => { + const result: ApiKeyDoc = await apiKeyService.findOneByActiveKey( + '123123123' + ); + + jest.spyOn(apiKeyService, 'findOneByActiveKey').mockReturnValueOnce( + null + ); + + expect(result).toBeFalsy(); + expect(result).toBeNull(); + }); + }); + + describe('getTotal', () => { + it('should return total data of apikeys', async () => { + const result: number = await apiKeyService.getTotal({ + name: apiKeyName1, + }); + + jest.spyOn(apiKeyService, 'getTotal').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(1); + }); + }); + + describe('active', () => { + it('should make apikey to be active', async () => { + const result: ApiKeyDoc = await apiKeyService.active(apiKey); + + jest.spyOn(apiKeyService, 'active').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKey._id); + expect(result.isActive).toBe(true); + }); + }); + + describe('inactive', () => { + it('should make apikey to be inactive', async () => { + const result: ApiKeyDoc = await apiKeyService.inactive(apiKey); + + jest.spyOn(apiKeyService, 'inactive').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKey._id); + expect(result.isActive).toBe(false); + }); + }); + + describe('create', () => { + it('should return a new apikeys', async () => { + const result: IApiKeyCreated = await apiKeyService.create({ + name: apiKeyName2, + }); + + jest.spyOn(apiKeyService, 'create').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.doc.name).toBe(apiKeyName2); + }); + + it('should return a new apikeys with expiration', async () => { + const result: IApiKeyCreated = await apiKeyService.create({ + name: apiKeyName3, + startDate, + endDate, + }); + + jest.spyOn(apiKeyService, 'create').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.doc.name).toBe(apiKeyName3); + }); + }); + + describe('createRaw', () => { + it('should return a new apikeys', async () => { + const result: IApiKeyCreated = await apiKeyService.createRaw({ + name: apiKeyName3, + description: faker.random.alphaNumeric(), + key: await apiKeyService.createKey(), + secret: await apiKeyService.createSecret(), + }); + + jest.spyOn(apiKeyService, 'createRaw').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.doc.name).toBe(apiKeyName3); + }); + + it('should return a new apikeys with expiration', async () => { + const result: IApiKeyCreated = await apiKeyService.createRaw({ + name: apiKeyName2, + description: faker.random.alphaNumeric(), + key: await apiKeyService.createKey(), + secret: await apiKeyService.createSecret(), + startDate, + endDate, + }); + + jest.spyOn(apiKeyService, 'createRaw').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.doc.name).toBe(apiKeyName2); + }); + }); + + describe('update', () => { + it('should return a updated apikey', async () => { + const nameUpdate = faker.random.alphaNumeric(10); + const result: ApiKeyDoc = await apiKeyService.update(apiKey, { + name: nameUpdate, + description: faker.random.alphaNumeric(20), + }); + jest.spyOn(apiKeyService, 'update').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKey._id); + expect(result.name).toBe(nameUpdate); + }); + }); + + describe('updateDate', () => { + it('should return a updated apikey', async () => { + const result: ApiKeyEntity = await apiKeyService.updateDate( + apiKey, + { + startDate, + endDate, + } + ); + jest.spyOn(apiKeyService, 'updateDate').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKey._id); + }); + }); + + describe('reset', () => { + it('old hashed should be difference with new hashed', async () => { + const hashOld: string = apiKey.hash; + const secret: string = await apiKeyService.createSecret(); + const result: ApiKeyDoc = await apiKeyService.reset(apiKey, secret); + jest.spyOn(apiKeyService, 'reset').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKey._id); + expect(result.hash).not.toBe(hashOld); + }); + }); + + describe('delete', () => { + it('should be success to delete', async () => { + const result: ApiKeyDoc = await apiKeyService.delete(apiKey); + + jest.spyOn(apiKeyService, 'delete').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(apiKey._id); + }); + }); + + describe('validateHashApiKey', () => { + it('should be succeed', async () => { + const result: boolean = await apiKeyService.validateHashApiKey( + apiKeyCreated.doc.hash, + apiKeyCreated.doc.hash + ); + jest.spyOn(apiKeyService, 'validateHashApiKey').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const result: boolean = await apiKeyService.validateHashApiKey( + apiKeyCreated.doc.hash, + faker.random.alphaNumeric(12) + ); + + jest.spyOn(apiKeyService, 'validateHashApiKey').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('createKey', () => { + it('should return a string', async () => { + const result: string = await apiKeyService.createKey(); + + jest.spyOn(apiKeyService, 'createKey').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('createSecret', () => { + it('should return a string', async () => { + const result: string = await apiKeyService.createSecret(); + + jest.spyOn(apiKeyService, 'createSecret').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('createKey', () => { + it('should return a hashed string', async () => { + const key1: string = await apiKeyService.createKey(); + const secret1: string = await apiKeyService.createSecret(); + const hashed: string = helperHashService.sha256( + `${key1}:${secret1}` + ); + const result: string = await apiKeyService.createHashApiKey( + key1, + secret1 + ); + + jest.spyOn(apiKeyService, 'createHashApiKey').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(hashed); + }); + }); + + describe('deleteMany', () => { + it('should be succeed', async () => { + const result: boolean = await apiKeyService.deleteMany({ + _id: apiKeyCreated.doc._id, + }); + + jest.spyOn(apiKeyService, 'deleteMany').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('inactiveManyByEndDate', () => { + it('should be succeed', async () => { + const result: boolean = await apiKeyService.inactiveManyByEndDate(); + + jest.spyOn( + apiKeyService, + 'inactiveManyByEndDate' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); +}); diff --git a/test/unit/auth/auth.service.spec.ts b/test/unit/auth/auth.service.spec.ts new file mode 100644 index 0000000..81f6785 --- /dev/null +++ b/test/unit/auth/auth.service.spec.ts @@ -0,0 +1,692 @@ +import { Test } from '@nestjs/testing'; +import { faker } from '@faker-js/faker'; +import { AuthService } from 'src/common/auth/services/auth.service'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import configs from 'src/configs'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { IAuthPassword } from 'src/common/auth/interfaces/auth.interface'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import { AuthModule } from 'src/common/auth/auth.module'; + +describe('AuthService', () => { + let authService: AuthService; + let configService: ConfigService; + let helperDateService: HelperDateService; + + let encryptedAccessToken: string; + let accessToken: string; + let encryptedRefreshToken: string; + let refreshToken: string; + let encryptedPermissionToken: string; + let permissionToken: string; + + let prefixAuthorization: string; + let accessTokenExpirationTime: number; + let refreshTokenExpirationTime: number; + let refreshTokenExpirationTimeRememberMe: number; + let issuer: string; + let audience: string; + let subject: string; + let payloadEncryption: boolean; + let permissionTokenExpirationTime: number; + + // cSpell:ignore ZfqgaDMPpWQ3lJEGQ8Ueu stnk + const user: Record = { + _id: '623cb7fd37a861a10bac2c91', + isActive: true, + salt: '$2b$08$GZfqgaDMPpWQ3lJEGQ8Ueu', + passwordExpired: new Date('2023-03-24T18:27:09.500Z'), + password: + '$2b$08$GZfqgaDMPpWQ3lJEGQ8Ueu1vJ3C6G3stnkS/5e61bK/4f1.Fuw2Eq', + role: { + _id: '623cb7f7965a74bf7a0e9e53', + accessFor: ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN, + isActive: true, + permissions: [], + name: 'admin', + }, + email: 'admin@mail.com', + mobileNumber: '08111111111', + lastName: 'test', + firstName: 'admin@mail.com', + }; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + AuthModule, + ], + }).compile(); + + authService = moduleRef.get(AuthService); + configService = moduleRef.get(ConfigService); + helperDateService = moduleRef.get(HelperDateService); + user.passwordExpired = helperDateService.forwardInDays(30); + + accessToken = await authService.createAccessToken(user); + encryptedAccessToken = await authService.encryptAccessToken(user); + refreshToken = await authService.createRefreshToken(user, { + notBeforeExpirationTime: 0, + }); + encryptedRefreshToken = await authService.encryptRefreshToken(user); + permissionToken = await authService.createPermissionToken(user); + encryptedPermissionToken = await authService.encryptPermissionToken( + user + ); + + prefixAuthorization = configService.get( + 'auth.prefixAuthorization' + ); + accessTokenExpirationTime = configService.get( + 'auth.accessToken.expirationTime' + ); + refreshTokenExpirationTime = configService.get( + 'auth.refreshToken.expirationTime' + ); + refreshTokenExpirationTimeRememberMe = configService.get( + 'auth.refreshToken.expirationTimeRememberMe' + ); + issuer = configService.get('auth.issuer'); + audience = configService.get('auth.audience'); + subject = configService.get('auth.subject'); + payloadEncryption = configService.get( + 'auth.payloadEncryption' + ); + permissionTokenExpirationTime = configService.get( + 'auth.permissionToken.expirationTime' + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', async () => { + expect(authService).toBeDefined(); + }); + + describe('encryptAccessToken', () => { + it('should be return a hashed string', async () => { + const result: string = await authService.encryptAccessToken(user); + + jest.spyOn(authService, 'encryptAccessToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('decryptAccessToken', () => { + it('should be return payload data', async () => { + const result: Record = + await authService.decryptAccessToken({ + data: encryptedAccessToken, + }); + + jest.spyOn(authService, 'decryptAccessToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(user._id); + }); + }); + + describe('createAccessToken', () => { + it('should be create access token in string, from object', async () => { + const result: string = await authService.createAccessToken(user); + + jest.spyOn(authService, 'createAccessToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + + it('should be create access token in string, from string', async () => { + const result: string = await authService.createAccessToken(''); + + jest.spyOn(authService, 'createAccessToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('validateAccessToken', () => { + it('should be verified', async () => { + const result: boolean = await authService.validateAccessToken( + accessToken + ); + + jest.spyOn(authService, 'validateAccessToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const result: boolean = await authService.validateAccessToken( + faker.random.alphaNumeric(20) + ); + + jest.spyOn(authService, 'validateAccessToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('payloadAccessToken', () => { + it('should given a payload of token', async () => { + const result: Record = + await authService.payloadAccessToken(accessToken); + + jest.spyOn(authService, 'payloadAccessToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.data._id).toBe(user._id); + }); + }); + + describe('encryptRefreshToken', () => { + it('should be return a hashed string', async () => { + const result: string = await authService.encryptRefreshToken(user); + + jest.spyOn(authService, 'encryptRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('decryptRefreshToken', () => { + it('should be return payload data', async () => { + const result: Record = + await authService.decryptRefreshToken({ + data: encryptedRefreshToken, + }); + + jest.spyOn(authService, 'decryptRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(user._id); + }); + }); + + describe('createRefreshToken', () => { + it('should be create refresh token in string, from object', async () => { + const result: string = await authService.createRefreshToken(user); + + jest.spyOn(authService, 'createRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + + it('should be create refresh token in string, from string', async () => { + const result: string = await authService.createRefreshToken(''); + + jest.spyOn(authService, 'createRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + + it('should be create refresh token in string with options rememberMe, from string', async () => { + const result: string = await authService.createRefreshToken(user, { + rememberMe: true, + }); + + jest.spyOn(authService, 'createRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('validateRefreshToken', () => { + it('should be verified', async () => { + const result: boolean = await authService.validateRefreshToken( + refreshToken + ); + + jest.spyOn(authService, 'validateRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const result: boolean = await authService.validateRefreshToken( + faker.random.alphaNumeric(20) + ); + + jest.spyOn(authService, 'validateRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('payloadRefreshToken', () => { + it('should given a payload of token', async () => { + const result: Record = + await authService.payloadRefreshToken(refreshToken); + + jest.spyOn(authService, 'payloadRefreshToken').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.data._id).toBe(user._id); + }); + }); + + describe('encryptPermissionToken', () => { + it('should be success', async () => { + const payloadHashedPermissionToken = + await authService.encryptPermissionToken(user); + jest.spyOn( + authService, + 'encryptPermissionToken' + ).mockImplementation(async () => payloadHashedPermissionToken); + + expect(await authService.encryptPermissionToken(user)).toBe( + payloadHashedPermissionToken + ); + }); + }); + + describe('decryptPermissionToken', () => { + it('should be return payload data', async () => { + const result: Record = + await authService.decryptPermissionToken({ + data: encryptedPermissionToken, + }); + + jest.spyOn( + authService, + 'decryptPermissionToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result._id).toBe(user._id); + }); + }); + + describe('createPermissionToken', () => { + it('should be create refresh token in string, from object', async () => { + const result: string = await authService.createPermissionToken( + user + ); + + jest.spyOn( + authService, + 'createPermissionToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('should be create refresh token in string, from string', async () => { + const result: string = await authService.createPermissionToken(''); + + jest.spyOn( + authService, + 'createPermissionToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + }); + + describe('validatePermissionToken', () => { + it('should be verified', async () => { + const result: boolean = await authService.validatePermissionToken( + permissionToken + ); + + jest.spyOn( + authService, + 'validatePermissionToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const result: boolean = await authService.validatePermissionToken( + faker.random.alphaNumeric(20) + ); + + jest.spyOn( + authService, + 'validatePermissionToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('payloadPermissionToken', () => { + it('should given a payload of token', async () => { + const result: Record = + await authService.payloadPermissionToken(permissionToken); + + jest.spyOn( + authService, + 'payloadPermissionToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result.data._id).toBe(user._id); + }); + }); + + describe('validateUser', () => { + it('should be a valid user', async () => { + const password = faker.internet.password(20, true, /[A-Za-z0-9]/); + const passwordHash = await authService.createPassword(password); + const result: boolean = await authService.validateUser( + password, + passwordHash.passwordHash + ); + + jest.spyOn(authService, 'validateUser').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be not valid user', async () => { + const password = faker.internet.password(20, true, /[A-Za-z0-9]/); + const passwordHash = await authService.createPassword(password); + const result: boolean = await authService.validateUser( + 'aasdasd12312', + passwordHash.passwordHash + ); + + jest.spyOn(authService, 'validateUser').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('createPayloadAccessToken', () => { + it('should be mapped', async () => { + const result: Record = + await authService.createPayloadAccessToken(user, false); + + jest.spyOn( + authService, + 'createPayloadAccessToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result._id).toBe(user._id); + }); + + it('payload with login date options', async () => { + const result: Record = + await authService.createPayloadAccessToken(user, false, { + loginDate: new Date(), + }); + + jest.spyOn( + authService, + 'createPayloadAccessToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result._id).toBe(user._id); + expect(result.loginDate).toBeDefined(); + }); + }); + + describe('createPayloadRefreshToken', () => { + it('should be mapped', async () => { + const result: Record = + await authService.createPayloadRefreshToken(user._id, false); + + jest.spyOn( + authService, + 'createPayloadRefreshToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result._id).toBe(user._id); + }); + + it('payload with login date options', async () => { + const result: Record = + await authService.createPayloadRefreshToken(user._id, false, { + loginDate: new Date(), + }); + + jest.spyOn( + authService, + 'createPayloadRefreshToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result._id).toBe(user._id); + expect(result.loginDate).toBeDefined(); + }); + }); + + describe('createPayloadPermissionToken', () => { + it('should be mapped', async () => { + const result: Record = + await authService.createPayloadPermissionToken(user); + + jest.spyOn( + authService, + 'createPayloadPermissionToken' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toEqual(user); + }); + }); + + describe('createPassword', () => { + it('should be success', async () => { + const password = faker.internet.password(20, true, /[A-Za-z0-9]/); + const result: IAuthPassword = await authService.createPassword( + password + ); + + jest.spyOn(authService, 'createPassword').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('checkPasswordExpired', () => { + it('should be not expired', async () => { + const result: boolean = await authService.checkPasswordExpired( + user.passwordExpired + ); + jest.spyOn(authService, 'checkPasswordExpired').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + + it('should be expired', async () => { + const expiredDate = new Date('1999-01-01'); + const result: boolean = await authService.checkPasswordExpired( + expiredDate + ); + + jest.spyOn(authService, 'checkPasswordExpired').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('getTokenType', () => { + it('should be success', async () => { + const result: string = await authService.getTokenType(); + + jest.spyOn(authService, 'getTokenType').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(prefixAuthorization); + }); + }); + + describe('getAccessTokenExpirationTime', () => { + it('should be give number in days for access token expiration', async () => { + const result: number = + await authService.getAccessTokenExpirationTime(); + + jest.spyOn( + authService, + 'getAccessTokenExpirationTime' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toBe(accessTokenExpirationTime); + }); + }); + + describe('getRefreshTokenExpirationTime', () => { + it('should be give number in days for refresh token expiration', async () => { + const result: number = + await authService.getRefreshTokenExpirationTime(); + + jest.spyOn( + authService, + 'getRefreshTokenExpirationTime' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toBe(refreshTokenExpirationTime); + }); + + it('should be give number in days for refresh token expiration with long period', async () => { + const result: number = + await authService.getRefreshTokenExpirationTime(true); + + jest.spyOn( + authService, + 'getRefreshTokenExpirationTime' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toBe(refreshTokenExpirationTimeRememberMe); + }); + }); + + describe('getIssuer', () => { + it('should be success', async () => { + const result: string = await authService.getIssuer(); + + jest.spyOn(authService, 'getIssuer').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(issuer); + }); + }); + + describe('getAudience', () => { + it('should be success', async () => { + const result: string = await authService.getAudience(); + + jest.spyOn(authService, 'getAudience').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(audience); + }); + }); + + describe('getSubject', () => { + it('should be success', async () => { + const result: string = await authService.getSubject(); + + jest.spyOn(authService, 'getSubject').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(subject); + }); + }); + + describe('getPayloadEncryption', () => { + it('should be success', async () => { + const result: boolean = await authService.getPayloadEncryption(); + + jest.spyOn(authService, 'getPayloadEncryption').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(payloadEncryption); + }); + }); + + describe('getPermissionTokenExpirationTime', () => { + it('should be success', async () => { + const result: number = + await authService.getPermissionTokenExpirationTime(); + + jest.spyOn( + authService, + 'getPermissionTokenExpirationTime' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toBe(permissionTokenExpirationTime); + }); + }); +}); diff --git a/test/unit/database/database.options.service.spec.ts b/test/unit/database/database.options.service.spec.ts new file mode 100644 index 0000000..7f9e0e6 --- /dev/null +++ b/test/unit/database/database.options.service.spec.ts @@ -0,0 +1,122 @@ +import { ConfigModule } from '@nestjs/config'; +import { MongooseModuleOptions } from '@nestjs/mongoose'; +import { Test } from '@nestjs/testing'; +import { DatabaseOptionsModule } from 'src/common/database/database.options.module'; +import { DatabaseOptionsService } from 'src/common/database/services/database.options.service'; +import { HelperModule } from 'src/common/helper/helper.module'; +import configs from 'src/configs'; + +describe('DatabaseOptionsService', () => { + let databaseOptionsService: DatabaseOptionsService; + + beforeEach(async () => { + process.env.APP_ENV = 'development'; + process.env.DATABASE_USER = 'nestUser'; + process.env.DATABASE_PASSWORD = 'nestUserTestPassword'; + process.env.DATABASE_DEBUG = 'true'; + process.env.DATABASE_OPTIONS = + 'replicaSet=rs0&retryWrites=true&w=majority'; + + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DatabaseOptionsModule, + ], + }).compile(); + + databaseOptionsService = moduleRef.get( + DatabaseOptionsService + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(databaseOptionsService).toBeDefined(); + }); + + describe('createOptions development', () => { + beforeEach(async () => { + process.env.APP_ENV = 'development'; + process.env.DATABASE_USER = 'nestUser'; + process.env.DATABASE_PASSWORD = 'nestUserTestPassword'; + process.env.DATABASE_DEBUG = 'true'; + process.env.DATABASE_OPTIONS = + 'replicaSet=rs0&retryWrites=true&w=majority'; + + await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DatabaseOptionsModule, + ], + }).compile(); + }); + + it('should be return mongoose options', async () => { + const result: MongooseModuleOptions = + databaseOptionsService.createOptions(); + + jest.spyOn( + databaseOptionsService, + 'createOptions' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBeTruthy(); + }); + }); + + describe('createOptions production', () => { + beforeEach(async () => { + process.env.APP_ENV = 'production'; + process.env.DATABASE_OPTIONS = ''; + + await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DatabaseOptionsModule, + ], + }).compile(); + }); + + it('should be return mongoose options with production env', async () => { + process.env.APP_ENV = 'production'; + process.env.DATABASE_OPTIONS = ''; + + const result: MongooseModuleOptions = + databaseOptionsService.createOptions(); + + jest.spyOn( + databaseOptionsService, + 'createOptions' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBeTruthy(); + expect(result).toBeTruthy(); + }); + }); +}); diff --git a/test/unit/debugger/debugger.options.service.spec.ts b/test/unit/debugger/debugger.options.service.spec.ts new file mode 100644 index 0000000..dea64c8 --- /dev/null +++ b/test/unit/debugger/debugger.options.service.spec.ts @@ -0,0 +1,199 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { DebuggerOptionsModule } from 'src/common/debugger/debugger.options.module'; +import { DebuggerOptionService } from 'src/common/debugger/services/debugger.options.service'; +import { HelperModule } from 'src/common/helper/helper.module'; +import configs from 'src/configs'; +import winston from 'winston'; + +describe('DebuggerOptionService ', () => { + let debuggerOptionService: DebuggerOptionService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DebuggerOptionsModule, + ], + }).compile(); + + debuggerOptionService = moduleRef.get( + DebuggerOptionService + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(debuggerOptionService).toBeDefined(); + }); + + describe('createLogger only write into console and file', () => { + beforeEach(async () => { + process.env.DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE = 'true'; + process.env.DEBUGGER_SYSTEM_WRITE_INTO_FILE = 'true'; + + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DebuggerOptionsModule, + ], + }).compile(); + + debuggerOptionService = moduleRef.get( + DebuggerOptionService + ); + }); + + it('must write into console and file', async () => { + const result: winston.LoggerOptions = + debuggerOptionService.createLogger(); + + jest.spyOn( + debuggerOptionService, + 'createLogger' + ).mockReturnValueOnce(result); + + expect(result).toBeDefined(); + expect(result).toBeTruthy(); + expect(Array.isArray(result.transports)).toBe(true); + expect((result.transports as []).length).toBe(4); + }); + }); + + describe('createLogger only write into console', () => { + beforeEach(async () => { + process.env.DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE = 'true'; + process.env.DEBUGGER_SYSTEM_WRITE_INTO_FILE = 'false'; + + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DebuggerOptionsModule, + ], + }).compile(); + + debuggerOptionService = moduleRef.get( + DebuggerOptionService + ); + }); + + it('must write into console ', async () => { + const result: winston.LoggerOptions = + debuggerOptionService.createLogger(); + + jest.spyOn( + debuggerOptionService, + 'createLogger' + ).mockReturnValueOnce(result); + + expect(result).toBeDefined(); + expect(result).toBeTruthy(); + expect(Array.isArray(result.transports)).toBe(true); + expect((result.transports as []).length).toBe(1); + }); + }); + + describe('createLogger only write into file', () => { + beforeEach(async () => { + process.env.DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE = 'false'; + process.env.DEBUGGER_SYSTEM_WRITE_INTO_FILE = 'true'; + + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DebuggerOptionsModule, + ], + }).compile(); + + debuggerOptionService = moduleRef.get( + DebuggerOptionService + ); + }); + + it('must write into file', async () => { + const result: winston.LoggerOptions = + debuggerOptionService.createLogger(); + + jest.spyOn( + debuggerOptionService, + 'createLogger' + ).mockReturnValueOnce(result); + + expect(result).toBeDefined(); + expect(result).toBeTruthy(); + expect(Array.isArray(result.transports)).toBe(true); + expect((result.transports as []).length).toBe(3); + }); + }); + + describe('createLogger write file and console inactive', () => { + beforeEach(async () => { + process.env.DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE = 'false'; + process.env.DEBUGGER_SYSTEM_WRITE_INTO_FILE = 'false'; + + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DebuggerOptionsModule, + ], + }).compile(); + + debuggerOptionService = moduleRef.get( + DebuggerOptionService + ); + }); + + it('must not write to file or console', async () => { + const result: winston.LoggerOptions = + debuggerOptionService.createLogger(); + + jest.spyOn( + debuggerOptionService, + 'createLogger' + ).mockReturnValueOnce(result); + + expect(result).toBeDefined(); + expect(result).toBeTruthy(); + expect(Array.isArray(result.transports)).toBe(true); + expect((result.transports as []).length).toBe(0); + }); + }); +}); diff --git a/test/unit/debugger/debugger.service.spec.ts b/test/unit/debugger/debugger.service.spec.ts new file mode 100644 index 0000000..b464e03 --- /dev/null +++ b/test/unit/debugger/debugger.service.spec.ts @@ -0,0 +1,174 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { DebuggerModule } from 'src/common/debugger/debugger.module'; +import { DebuggerService } from 'src/common/debugger/services/debugger.service'; +import { HelperModule } from 'src/common/helper/helper.module'; +import configs from 'src/configs'; + +describe('DebuggerService', () => { + let debuggerService: DebuggerService; + + const sDescription = 'test description'; + const sClass = 'test class'; + const cFunction = 'test function'; + const data = { test: 'test' }; + + beforeEach(async () => { + process.env.DEBUGGER_HTTP_WRITE_INTO_CONSOLE = 'true'; + process.env.DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE = 'true'; + process.env.DEBUGGER_HTTP_WRITE_INTO_FILE = 'true'; + process.env.DEBUGGER_SYSTEM_WRITE_INTO_FILE = 'true'; + + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + DebuggerModule.forRoot(), + ], + }).compile(); + + debuggerService = moduleRef.get(DebuggerService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(debuggerService).toBeDefined(); + }); + + describe('info', () => { + it('should write into log', async () => { + const result: void = debuggerService.info('DebuggerService', { + description: sDescription, + class: sClass, + function: cFunction, + }); + + jest.spyOn(debuggerService, 'info').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + + it('should write into log with data', async () => { + const result: void = debuggerService.info( + 'DebuggerService', + { + description: sDescription, + class: sClass, + function: cFunction, + }, + data + ); + + jest.spyOn(debuggerService, 'info').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + }); + + describe('debug', () => { + it('should write into log', async () => { + const result: void = debuggerService.debug('DebuggerService', { + description: sDescription, + class: sClass, + function: cFunction, + }); + + jest.spyOn(debuggerService, 'debug').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + + it('should write into log with data', async () => { + const result: void = debuggerService.debug( + 'DebuggerService', + { + description: sDescription, + class: sClass, + function: cFunction, + }, + data + ); + + jest.spyOn(debuggerService, 'debug').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + }); + + describe('warn', () => { + it('should write into log', async () => { + const result: void = debuggerService.warn('DebuggerService', { + description: sDescription, + class: sClass, + function: cFunction, + }); + + jest.spyOn(debuggerService, 'warn').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + + it('should write into log with data', async () => { + const result: void = debuggerService.warn( + 'DebuggerService', + { + description: sDescription, + class: sClass, + function: cFunction, + }, + data + ); + + jest.spyOn(debuggerService, 'warn').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + }); + + describe('error', () => { + it('should write into log', async () => { + const result: void = debuggerService.error('DebuggerService', { + description: sDescription, + class: sClass, + function: cFunction, + }); + + jest.spyOn(debuggerService, 'error').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + + it('should write into log with data', async () => { + const result: void = debuggerService.error( + 'DebuggerService', + { + description: sDescription, + class: sClass, + function: cFunction, + }, + data + ); + + jest.spyOn(debuggerService, 'error').mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/test/unit/helper/helper.array.service.spec.ts b/test/unit/helper/helper.array.service.spec.ts new file mode 100644 index 0000000..5e70837 --- /dev/null +++ b/test/unit/helper/helper.array.service.spec.ts @@ -0,0 +1,503 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { IHelperArrayRemove } from 'src/common/helper/interfaces/helper.interface'; +import { HelperArrayService } from 'src/common/helper/services/helper.array.service'; +import configs from 'src/configs'; + +describe('HelperArrayService', () => { + let helperArrayService: HelperArrayService; + let arrays: (string | number)[]; + let arraysString: string; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperArrayService = + moduleRef.get(HelperArrayService); + + arrays = [1, '2', '3', 3, 1, 6, 7, 8]; + arraysString = '1,2,3,3,1,6,7,8'; + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(helperArrayService).toBeDefined(); + }); + + describe('getLeftByIndex', () => { + it('should be return a value by index from left', async () => { + const result: number | string = helperArrayService.getLeftByIndex< + number | string + >(arrays, 1); + + jest.spyOn( + helperArrayService, + 'getLeftByIndex' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(arrays[1]); + }); + }); + + describe('getRightByIndex', () => { + it('should be return a value by index from right', async () => { + const result: number | string = helperArrayService.getRightByIndex< + number | string + >(arrays, 1); + + jest.spyOn( + helperArrayService, + 'getRightByIndex' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(arrays[arrays.length - 1]); + }); + }); + + describe('getLeftByLength', () => { + it('should be return a array by 1 length from left', async () => { + const result: (number | string)[] = + helperArrayService.getLeftByLength(arrays, 1); + + jest.spyOn( + helperArrayService, + 'getLeftByLength' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toEqual([arrays[0]]); + expect(result[0]).toBe(arrays[0]); + }); + + it('should be return a array by 3 length from left', async () => { + const result: (number | string)[] = + helperArrayService.getLeftByLength(arrays, 3); + + jest.spyOn( + helperArrayService, + 'getLeftByLength' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toEqual([arrays[0], arrays[1], arrays[2]]); + expect(result[0]).toBe(arrays[0]); + expect(result[2]).toBe(arrays[2]); + }); + }); + + describe('getRightByLength', () => { + it('should be return a array by 1 length from right', async () => { + const result: (number | string)[] = + helperArrayService.getRightByLength(arrays, 1); + + jest.spyOn( + helperArrayService, + 'getRightByLength' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toEqual([arrays[arrays.length - 1]]); + expect(result[0]).toBe(arrays[arrays.length - 1]); + }); + + it('should be return a array by 3 length from right', async () => { + const result: (number | string)[] = + helperArrayService.getRightByLength(arrays, 3); + + jest.spyOn( + helperArrayService, + 'getRightByLength' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toEqual([ + arrays[arrays.length - 3], + arrays[arrays.length - 2], + arrays[arrays.length - 1], + ]); + expect(result[0]).toBe(arrays[arrays.length - 3]); + expect(result[2]).toBe(arrays[arrays.length - 1]); + }); + }); + + describe('getLast', () => { + it('should be return a last data of array', async () => { + const result: number | string = helperArrayService.getLast< + number | string + >(arrays); + + jest.spyOn(helperArrayService, 'getLast').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toEqual(arrays[arrays.length - 1]); + }); + }); + + describe('getFirst', () => { + it('should be return a first data of array', async () => { + const result: number | string = helperArrayService.getFirst< + number | string + >(arrays); + + jest.spyOn(helperArrayService, 'getFirst').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toEqual(arrays[0]); + }); + }); + + describe('getFirstIndexByValue', () => { + it('should be return a first index with search by value', async () => { + const result: number = helperArrayService.getFirstIndexByValue< + number | string + >(arrays, '2'); + + jest.spyOn( + helperArrayService, + 'getFirstIndexByValue' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toEqual(1); + }); + }); + + describe('getLastIndexByValue', () => { + it('should be return a last index with search by value', async () => { + const result: number = helperArrayService.getLastIndexByValue< + number | string + >(arrays, 1); + + jest.spyOn( + helperArrayService, + 'getLastIndexByValue' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toEqual(4); + }); + }); + + describe('removeByValue', () => { + it('should remove array by searching a value', async () => { + const result: IHelperArrayRemove = + helperArrayService.removeByValue(arrays, 8); + + jest.spyOn(helperArrayService, 'removeByValue').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result.removed).toBeDefined(); + expect(result.arrays).toBeDefined(); + expect(result.removed.length).toEqual(1); + expect(result.removed.length).toEqual(1); + expect(result.removed[0]).toEqual(8); + expect(result.arrays.length).toEqual(7); + expect(result.arrays[result.arrays.length - 1]).toBe(7); + }); + }); + + describe('removeLeftByLength', () => { + it('should remove array by length from left', async () => { + const result: (number | string)[] = + helperArrayService.removeLeftByLength( + arrays, + 1 + ); + + jest.spyOn( + helperArrayService, + 'removeLeftByLength' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toEqual(arrays.length - 1); + expect(result[0]).toBe('2'); + }); + }); + + describe('removeRightByLength', () => { + it('should remove array by length from right', async () => { + const result: (number | string)[] = + helperArrayService.removeRightByLength( + arrays, + 1 + ); + + jest.spyOn( + helperArrayService, + 'removeRightByLength' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toEqual(arrays.length - 1); + expect(result[result.length - 1]).toBe(7); + }); + }); + + describe('joinToString', () => { + it('should join array to string', async () => { + const result: string = helperArrayService.joinToString< + number | string + >(arrays, ','); + + jest.spyOn(helperArrayService, 'joinToString').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(arraysString); + }); + }); + + describe('reverse', () => { + it('array should be reversed', async () => { + const result: (number | string)[] = helperArrayService.reverse< + number | string + >(arrays); + + jest.spyOn(helperArrayService, 'reverse').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result[0]).toBe(8); + expect(result[result.length - 1]).toBe(1); + }); + }); + + describe('unique', () => { + it('array should be unique', async () => { + const result: (number | string)[] = helperArrayService.unique< + number | string + >(arrays); + + jest.spyOn(helperArrayService, 'unique').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result.length).toBe(7); + }); + }); + + describe('shuffle', () => { + it('array should be shuffle', async () => { + const result: (number | string)[] = helperArrayService.shuffle< + number | string + >(arrays); + + jest.spyOn(helperArrayService, 'shuffle').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result.length).toBe(arrays.length); + expect(result.every((val) => arrays.includes(val))).toBe(true); + expect(result.every((val, idx) => arrays[idx] === val)).toBe(false); + }); + }); + + describe('merge', () => { + it('array should be merger', async () => { + const result: (number | string)[] = helperArrayService.merge< + number | string + >(arrays, arrays); + + jest.spyOn(helperArrayService, 'merge').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toBe(arrays.length * 2); + }); + }); + + describe('mergeUnique', () => { + it('array should be merger and unique array', async () => { + const result: (number | string)[] = helperArrayService.mergeUnique< + number | string + >(arrays, arrays); + + jest.spyOn(helperArrayService, 'mergeUnique').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result.length).toBe(7); + }); + }); + + describe('filterIncludeByValue', () => { + it('should be return filtered value', async () => { + const result: (number | string)[] = + helperArrayService.filterIncludeByValue( + arrays, + 1 + ); + + jest.spyOn( + helperArrayService, + 'filterIncludeByValue' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toBe(2); + expect(result.every((val) => val === 1)).toBe(true); + }); + }); + + describe('filterNotIncludeByValue', () => { + it('should be return filtered value', async () => { + const result: (number | string)[] = + helperArrayService.filterNotIncludeByValue( + arrays, + 1 + ); + + jest.spyOn( + helperArrayService, + 'filterNotIncludeByValue' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toBe(6); + expect(result.every((val) => val !== 1)).toBe(true); + }); + }); + + describe('filterNotIncludeUniqueByArray', () => { + it('should be return filtered value', async () => { + const result: (number | string)[] = + helperArrayService.filterNotIncludeUniqueByArray< + number | string + >(arrays, [1]); + + jest.spyOn( + helperArrayService, + 'filterNotIncludeUniqueByArray' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toBe(6); + expect(result.every((val) => val !== 1)).toBe(true); + }); + }); + + describe('filterIncludeUniqueByArray', () => { + it('should be return filtered value', async () => { + const result: (number | string)[] = + helperArrayService.filterIncludeUniqueByArray( + arrays, + [1] + ); + + jest.spyOn( + helperArrayService, + 'filterIncludeUniqueByArray' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toBe(1); + expect(result.every((val) => val === 1)).toBe(true); + }); + }); + + describe('equals', () => { + it('should be equals', async () => { + const result: boolean = helperArrayService.equals(arrays, arrays); + + jest.spyOn(helperArrayService, 'equals').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('notEquals', () => { + it('should be equals', async () => { + const result: boolean = helperArrayService.notEquals( + arrays, + [1, 2, 3] + ); + + jest.spyOn(helperArrayService, 'notEquals').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('in', () => { + it('value must in arrays', async () => { + const result: boolean = helperArrayService.in(arrays, [1]); + + jest.spyOn(helperArrayService, 'in').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('notIn', () => { + it('value must not in arrays', async () => { + const result: boolean = helperArrayService.notIn(arrays, ['z']); + + jest.spyOn(helperArrayService, 'notIn').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('includes', () => { + it('value must includes arrays', async () => { + const result: boolean = helperArrayService.includes(arrays, 1); + + jest.spyOn(helperArrayService, 'includes').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('chunk', () => { + it('array chunk to be x value', async () => { + const result: (number | string)[][] = helperArrayService.chunk( + arrays, + 2 + ); + + jest.spyOn(helperArrayService, 'chunk').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.length).toBe(4); + }); + }); +}); diff --git a/test/unit/helper/helper.date.service.spec.ts b/test/unit/helper/helper.date.service.spec.ts new file mode 100644 index 0000000..f83b241 --- /dev/null +++ b/test/unit/helper/helper.date.service.spec.ts @@ -0,0 +1,974 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { + ENUM_HELPER_DATE_DIFF, + ENUM_HELPER_DATE_FORMAT, +} from 'src/common/helper/constants/helper.enum.constant'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { + IHelperDateExtractDate, + IHelperDateStartAndEndDate, +} from 'src/common/helper/interfaces/helper.interface'; +import { HelperDateService } from 'src/common/helper/services/helper.date.service'; +import configs from 'src/configs'; + +describe('HelperDateService', () => { + let helperDateService: HelperDateService; + const dateString = '2000-01-01'; + const date1: Date = new Date('2000-01-01'); + const date2: Date = new Date('2010-01-10'); + const dateTimestamp: number = date2.valueOf(); + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperDateService = moduleRef.get(HelperDateService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(helperDateService).toBeDefined(); + }); + + describe('calculateAge', () => { + it('return age in number', async () => { + const result: number = helperDateService.calculateAge(date1); + + jest.spyOn(helperDateService, 'calculateAge').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('diff', () => { + it('should be return a number of days differences', async () => { + const result: number = helperDateService.diff(date1, date2); + + jest.spyOn(helperDateService, 'diff').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a number of minutes differences', async () => { + const result: number = helperDateService.diff(date1, date2, { + format: ENUM_HELPER_DATE_DIFF.MINUTES, + }); + + jest.spyOn(helperDateService, 'diff').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a number of minutes differences', async () => { + const result: number = helperDateService.diff(date1, date2, { + format: ENUM_HELPER_DATE_DIFF.HOURS, + }); + + jest.spyOn(helperDateService, 'diff').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a number of minutes days', async () => { + const result: number = helperDateService.diff(date1, date2, { + format: ENUM_HELPER_DATE_DIFF.DAYS, + }); + + jest.spyOn(helperDateService, 'diff').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a number of seconds differences', async () => { + const result: number = helperDateService.diff(date1, date2, { + format: ENUM_HELPER_DATE_DIFF.SECONDS, + }); + + jest.spyOn(helperDateService, 'diff').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a number of milis differences', async () => { + const result: number = helperDateService.diff(date1, date2, { + format: ENUM_HELPER_DATE_DIFF.MILIS, + }); + + jest.spyOn(helperDateService, 'diff').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('check', () => { + it('string must be a valid date', async () => { + const result: boolean = helperDateService.check(dateString); + + jest.spyOn(helperDateService, 'check').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('date must be a valid date', async () => { + const result: boolean = helperDateService.check(date1); + + jest.spyOn(helperDateService, 'check').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('number must be a valid date', async () => { + const result: boolean = helperDateService.check(dateTimestamp); + + jest.spyOn(helperDateService, 'check').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('checkTimestamp', () => { + it('number must be a valid timestamp', async () => { + const result: boolean = + helperDateService.checkTimestamp(dateTimestamp); + + jest.spyOn(helperDateService, 'checkTimestamp').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('create', () => { + it('should be create date base on today', async () => { + const result: Date = helperDateService.create(); + + jest.spyOn(helperDateService, 'create').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be create date base on date parameter', async () => { + const result: Date = helperDateService.create(date1); + + jest.spyOn(helperDateService, 'create').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be create date base on today and force the time to start of day', async () => { + const result: Date = helperDateService.create(null, { + startOfDay: true, + }); + + jest.spyOn(helperDateService, 'create').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('timestamp', () => { + it('should be create timestamp base on today', async () => { + const result: number = helperDateService.timestamp(); + + jest.spyOn(helperDateService, 'timestamp').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be create timestamp base on date parameter', async () => { + const result: number = helperDateService.timestamp(date1); + + jest.spyOn(helperDateService, 'timestamp').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be create timestamp base on today and force the time to start of day', async () => { + const result: number = helperDateService.timestamp(null, { + startOfDay: true, + }); + + jest.spyOn(helperDateService, 'timestamp').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('format', () => { + it('should be return a day as string, default', async () => { + const result: string = helperDateService.format(date1); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe('2000-01-01'); + }); + + it('should be return a day as string, format date', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.DATE, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe('2000-01-01'); + }); + + it('should be return a day as string, format friendly date', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.FRIENDLY_DATE, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format friendly date time', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.FRIENDLY_DATE_TIME, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format year and month only', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.YEAR_MONTH, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe('2000-01'); + }); + + it('should be return a day as string, format month and date only', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.MONTH_DATE, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe('01-01'); + }); + + it('should be return a day as string, format only year', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.ONLY_YEAR, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe('2000'); + }); + + it('should be return a day as string, format only month', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.ONLY_MONTH, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe('01'); + }); + + it('should be return a day as string, format only date', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.ONLY_DATE, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe('01'); + }); + + it('should be return a day as string, format iso date', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.ISO_DATE, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only day and long version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.DAY_LONG, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only day and short version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.DAY_SHORT, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only hour and long version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.HOUR_LONG, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only hour and short version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.HOUR_SHORT, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only minute and long version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.MINUTE_LONG, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only minute and short version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.MINUTE_SHORT, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only second and long version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.SECOND_LONG, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be return a day as string, format only second and short version', async () => { + const result: string = helperDateService.format(date1, { + format: ENUM_HELPER_DATE_FORMAT.SECOND_SHORT, + }); + + jest.spyOn(helperDateService, 'format').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('forwardInMilliseconds', () => { + it('should be forward to 2 milis from today', async () => { + const result: Date = helperDateService.forwardInMilliseconds(2); + + jest.spyOn( + helperDateService, + 'forwardInMilliseconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be forward to 2 milis from x date', async () => { + const result: Date = helperDateService.forwardInMilliseconds(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'forwardInMilliseconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('backwardInMilliseconds', () => { + it('should be backward to 2 milis from today', async () => { + const result: Date = helperDateService.backwardInMilliseconds(2); + + jest.spyOn( + helperDateService, + 'backwardInMilliseconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be backward to 2 milis from x date', async () => { + const result: Date = helperDateService.backwardInMilliseconds(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'backwardInMilliseconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('forwardInSeconds', () => { + it('should be forward to 2 seconds from today', async () => { + const result: Date = helperDateService.forwardInSeconds(2); + + jest.spyOn( + helperDateService, + 'forwardInSeconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be forward to 2 seconds from x date', async () => { + const result: Date = helperDateService.forwardInSeconds(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'forwardInSeconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('backwardInSeconds', () => { + it('should be backward to 2 seconds from today', async () => { + const result: Date = helperDateService.backwardInSeconds(2); + + jest.spyOn( + helperDateService, + 'backwardInSeconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be backward to 2 seconds from x date', async () => { + const result: Date = helperDateService.backwardInSeconds(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'backwardInSeconds' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('forwardInMinutes', () => { + it('should be forward to 2 minutes from today', async () => { + const result: Date = helperDateService.forwardInMinutes(2); + + jest.spyOn( + helperDateService, + 'forwardInMinutes' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be forward to 2 minutes from x date', async () => { + const result: Date = helperDateService.forwardInMinutes(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'forwardInMinutes' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('backwardInMinutes', () => { + it('should be backward to 2 minutes from today', async () => { + const result: Date = helperDateService.backwardInMinutes(2); + + jest.spyOn( + helperDateService, + 'backwardInMinutes' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be backward to 2 minutes from x date', async () => { + const result: Date = helperDateService.backwardInMinutes(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'backwardInMinutes' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('forwardInHours', () => { + it('should be forward to 2 hours from today', async () => { + const result: Date = helperDateService.forwardInHours(2); + + jest.spyOn(helperDateService, 'forwardInHours').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be forward to 2 hours from x date', async () => { + const result: Date = helperDateService.forwardInHours(2, { + fromDate: date1, + }); + + jest.spyOn(helperDateService, 'forwardInHours').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('backwardInHours', () => { + it('should be backward to 2 hours from today', async () => { + const result: Date = helperDateService.backwardInHours(2); + + jest.spyOn( + helperDateService, + 'backwardInHours' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be backward to 2 hours from x date', async () => { + const result: Date = helperDateService.backwardInHours(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'backwardInHours' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('forwardInDays', () => { + it('should be forward to 2 days from today', async () => { + const result: Date = helperDateService.forwardInDays(2); + + jest.spyOn(helperDateService, 'forwardInDays').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be forward to 2 days from x date', async () => { + const result: Date = helperDateService.forwardInDays(2, { + fromDate: date1, + }); + + jest.spyOn(helperDateService, 'forwardInDays').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('backwardInDays', () => { + it('should be backward to 2 days from today', async () => { + const result: Date = helperDateService.backwardInDays(2); + + jest.spyOn(helperDateService, 'backwardInDays').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be backward to 2 days from x date', async () => { + const result: Date = helperDateService.backwardInDays(2, { + fromDate: date1, + }); + + jest.spyOn(helperDateService, 'backwardInDays').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('forwardInMonths', () => { + it('should be forward to 2 months from today', async () => { + const result: Date = helperDateService.forwardInMonths(2); + + jest.spyOn( + helperDateService, + 'forwardInMonths' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be forward to 2 months from x date', async () => { + const result: Date = helperDateService.forwardInMonths(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'forwardInMonths' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('backwardInMonths', () => { + it('should be backward to 2 months from today', async () => { + const result: Date = helperDateService.backwardInMonths(2); + + jest.spyOn( + helperDateService, + 'backwardInMonths' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be backward to 2 months from x date', async () => { + const result: Date = helperDateService.backwardInMonths(2, { + fromDate: date1, + }); + + jest.spyOn( + helperDateService, + 'backwardInMonths' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('endOfMonth', () => { + it('return date end of current year', async () => { + const result: Date = helperDateService.endOfMonth(); + + jest.spyOn(helperDateService, 'endOfMonth').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('return date end of x year', async () => { + const result: Date = helperDateService.endOfMonth(date1); + + jest.spyOn(helperDateService, 'endOfMonth').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('startOfMonth', () => { + it('return date start of current year', async () => { + const result: Date = helperDateService.startOfMonth(); + + jest.spyOn(helperDateService, 'startOfMonth').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('return date start of x year', async () => { + const result: Date = helperDateService.startOfMonth(date1); + + jest.spyOn(helperDateService, 'startOfMonth').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('endOfYear', () => { + it('return date end of current year', async () => { + const result: Date = helperDateService.endOfYear(); + + jest.spyOn(helperDateService, 'endOfYear').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('return date end of x year', async () => { + const result: Date = helperDateService.endOfYear(date1); + + jest.spyOn(helperDateService, 'endOfYear').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('startOfYear', () => { + it('return date start of current year', async () => { + const result: Date = helperDateService.startOfYear(); + + jest.spyOn(helperDateService, 'startOfYear').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('return date start of x year', async () => { + const result: Date = helperDateService.startOfYear(date1); + + jest.spyOn(helperDateService, 'startOfYear').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('endOfDay', () => { + it('return date end of current year', async () => { + const result: Date = helperDateService.endOfDay(); + + jest.spyOn(helperDateService, 'endOfDay').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('return date end of x year', async () => { + const result: Date = helperDateService.endOfDay(date1); + + jest.spyOn(helperDateService, 'endOfDay').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('startOfDay', () => { + it('return date start of current year', async () => { + const result: Date = helperDateService.startOfDay(); + + jest.spyOn(helperDateService, 'startOfDay').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('return date start of x year', async () => { + const result: Date = helperDateService.startOfDay(date1); + + jest.spyOn(helperDateService, 'startOfDay').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('extractDate', () => { + it('should extract a date to dat, month, and year as number', async () => { + const result: IHelperDateExtractDate = + helperDateService.extractDate(date1); + + jest.spyOn(helperDateService, 'extractDate').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result.day).toBe('01'); + expect(result.month).toBe('01'); + expect(result.year).toBe('2000'); + }); + }); + + describe('roundDown', () => { + it('should be round down a milis from date', async () => { + const result: Date = helperDateService.roundDown(date1); + + jest.spyOn(helperDateService, 'roundDown').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be round down a milis and hours from date', async () => { + const result: Date = helperDateService.roundDown(date1, { + hour: true, + minute: false, + second: false, + }); + + jest.spyOn(helperDateService, 'roundDown').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be round down a milis and minutes from date', async () => { + const result: Date = helperDateService.roundDown(date1, { + hour: false, + minute: true, + second: false, + }); + + jest.spyOn(helperDateService, 'roundDown').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be round down a milis and seconds from date', async () => { + const result: Date = helperDateService.roundDown(date1, { + hour: false, + minute: false, + second: true, + }); + + jest.spyOn(helperDateService, 'roundDown').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('getStartAndEndDate', () => { + it('should be success', async () => { + const result: IHelperDateStartAndEndDate = + helperDateService.getStartAndEndDate(); + + jest.spyOn( + helperDateService, + 'getStartAndEndDate' + ).mockReturnValueOnce(result); + + const dt = helperDateService.create(); + const start = helperDateService.startOfYear(dt); + const end = helperDateService.endOfYear(dt); + + expect(result).toBeTruthy(); + expect(`${result.startDate}`).toBe(`${start}`); + expect(`${result.endDate}`).toBe(`${end}`); + }); + + it('should be success options year', async () => { + const result: IHelperDateStartAndEndDate = + helperDateService.getStartAndEndDate({ year: 2022 }); + + jest.spyOn( + helperDateService, + 'getStartAndEndDate' + ).mockReturnValueOnce(result); + + const dt = helperDateService.create('2022-01-02'); + const start = helperDateService.startOfYear(dt); + const end = helperDateService.endOfYear(dt); + + expect(result).toBeTruthy(); + expect(`${result.startDate}`).toBe(`${start}`); + expect(`${result.endDate}`).toBe(`${end}`); + }); + + it('should be success options year and month', async () => { + const result: IHelperDateStartAndEndDate = + helperDateService.getStartAndEndDate({ year: 2022, month: 2 }); + + jest.spyOn( + helperDateService, + 'getStartAndEndDate' + ).mockReturnValueOnce(result); + + const dt = helperDateService.create('2022-02-02'); + const start = helperDateService.startOfMonth(dt); + const end = helperDateService.endOfMonth(dt); + + expect(result).toBeTruthy(); + expect(`${result.startDate}`).toBe(`${start}`); + expect(`${result.endDate}`).toBe(`${end}`); + }); + }); +}); diff --git a/test/unit/helper/helper.encryption.service.spec.ts b/test/unit/helper/helper.encryption.service.spec.ts new file mode 100644 index 0000000..4dd7f13 --- /dev/null +++ b/test/unit/helper/helper.encryption.service.spec.ts @@ -0,0 +1,250 @@ +import { faker } from '@faker-js/faker'; +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { HelperEncryptionService } from 'src/common/helper/services/helper.encryption.service'; +import configs from 'src/configs'; + +describe('HelperEncryptionService', () => { + let helperEncryptionService: HelperEncryptionService; + const audience = 'https://example.com'; + const issuer = 'nest'; + const subject = 'nest'; + const data = 'aaaa'; + const dataObject = { test: 'aaaa' }; + let enBase64: string; + let enAes256: string; + let enAes256Object: string; + let enJwt: string; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperEncryptionService = moduleRef.get( + HelperEncryptionService + ); + + enBase64 = helperEncryptionService.base64Encrypt(data); + enAes256 = helperEncryptionService.aes256Encrypt( + data, + '1234567', + '1231231231231231' + ); + enAes256Object = helperEncryptionService.aes256Encrypt( + dataObject, + '1234567', + '1231231231231231' + ); + enJwt = helperEncryptionService.jwtEncrypt( + { data }, + { expiredIn: '1h', secretKey: data, audience, issuer, subject } + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(helperEncryptionService).toBeDefined(); + }); + + describe('base64Encrypt', () => { + it('string must encode to base64 string', async () => { + const result: string = helperEncryptionService.base64Encrypt(data); + + jest.spyOn( + helperEncryptionService, + 'base64Encrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('base64Decrypt', () => { + it('base64 string must decode string', async () => { + const result: string = + helperEncryptionService.base64Decrypt(enBase64); + + jest.spyOn( + helperEncryptionService, + 'base64Decrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('base64Compare', () => { + it('should be success', async () => { + const result: boolean = helperEncryptionService.base64Compare( + enBase64, + enBase64 + ); + + jest.spyOn( + helperEncryptionService, + 'base64Compare' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const result: boolean = helperEncryptionService.base64Compare( + data, + enBase64 + ); + + jest.spyOn( + helperEncryptionService, + 'base64Compare' + ).mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('aes256Encrypt', () => { + it('string must be encode to aes256 string', async () => { + const result: string = helperEncryptionService.aes256Encrypt( + data, + '1234567', + '1231231231231231' + ); + + jest.spyOn( + helperEncryptionService, + 'aes256Encrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('object must be encode to aes256 string', async () => { + const result: string = helperEncryptionService.aes256Encrypt( + dataObject, + '1234567', + '1231231231231231' + ); + + jest.spyOn( + helperEncryptionService, + 'aes256Encrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('aes256Decrypt', () => { + it('aes256 string decode to string', async () => { + const result: string = helperEncryptionService.aes256Decrypt( + enAes256, + '1234567', + '1231231231231231' + ) as string; + + jest.spyOn( + helperEncryptionService, + 'aes256Decrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be success object', async () => { + const result: Record = + helperEncryptionService.aes256Decrypt( + enAes256Object, + '1234567', + '1231231231231231' + ) as Record; + + jest.spyOn( + helperEncryptionService, + 'aes256Decrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('jwtEncrypt', () => { + it('should be success to jwt encode string', async () => { + const result: string = helperEncryptionService.jwtEncrypt( + { data }, + { expiredIn: '1h', secretKey: data, audience, issuer, subject } + ); + + jest.spyOn( + helperEncryptionService, + 'jwtEncrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('jwtDecrypt', () => { + it('jwt encode string decode and give a payload', async () => { + const result: Record = + helperEncryptionService.jwtDecrypt(enJwt); + + jest.spyOn( + helperEncryptionService, + 'jwtDecrypt' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('jwtVerify', () => { + it('should be success', async () => { + const result: boolean = helperEncryptionService.jwtVerify(enJwt, { + secretKey: data, + audience, + issuer, + subject, + }); + + jest.spyOn( + helperEncryptionService, + 'jwtVerify' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be failed', async () => { + const result: boolean = helperEncryptionService.jwtVerify(enJwt, { + secretKey: faker.random.alpha(5), + audience, + issuer, + subject, + }); + + jest.spyOn( + helperEncryptionService, + 'jwtVerify' + ).mockReturnValueOnce(result); + + expect(result).toBeFalsy(); + }); + }); +}); diff --git a/test/unit/helper/helper.file.service.spec.ts b/test/unit/helper/helper.file.service.spec.ts new file mode 100644 index 0000000..fc68429 --- /dev/null +++ b/test/unit/helper/helper.file.service.spec.ts @@ -0,0 +1,112 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { IHelperFileRows } from 'src/common/helper/interfaces/helper.interface'; +import { HelperFileService } from 'src/common/helper/services/helper.file.service'; +import configs from 'src/configs'; +import { WorkBook } from 'xlsx'; + +describe('HelperFileService', () => { + let helperFileService: HelperFileService; + let workbook: WorkBook; + let file: Buffer; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperFileService = moduleRef.get(HelperFileService); + + workbook = helperFileService.createExcelWorkbook([ + { test: 1 }, + { test: 2 }, + ]); + file = helperFileService.writeExcelToBuffer(workbook); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(helperFileService).toBeDefined(); + }); + + describe('createExcelWorkbook', () => { + it('should be convert array of object to excel workbook', async () => { + const result: WorkBook = helperFileService.createExcelWorkbook([ + { test: 1 }, + { test: 2 }, + ]); + + jest.spyOn( + helperFileService, + 'createExcelWorkbook' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be convert empty array to excel workbook', async () => { + const result: WorkBook = helperFileService.createExcelWorkbook([]); + + jest.spyOn( + helperFileService, + 'createExcelWorkbook' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('writeExcelToBuffer', () => { + it('write workbook to buffer', async () => { + const result: Buffer = + helperFileService.writeExcelToBuffer(workbook); + + jest.spyOn( + helperFileService, + 'writeExcelToBuffer' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('readExcelFromBuffer', () => { + it('should be success', async () => { + const result: IHelperFileRows[] = + helperFileService.readExcelFromBuffer(file); + + jest.spyOn( + helperFileService, + 'readExcelFromBuffer' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('convertToBytes', () => { + it('should be success', async () => { + const result: number = helperFileService.convertToBytes('1mb'); + + jest.spyOn(helperFileService, 'convertToBytes').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(1048576); + }); + }); +}); diff --git a/test/unit/helper/helper.geo.service.spec.ts b/test/unit/helper/helper.geo.service.spec.ts new file mode 100644 index 0000000..2ac7924 --- /dev/null +++ b/test/unit/helper/helper.geo.service.spec.ts @@ -0,0 +1,54 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { HelperGeoService } from 'src/common/helper/services/helper.geo.service'; +import configs from 'src/configs'; + +describe('HelperGeoService', () => { + let helperGeoService: HelperGeoService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperGeoService = moduleRef.get(HelperGeoService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(HelperGeoService).toBeDefined(); + }); + + describe('inRadius', () => { + it('should be success', async () => { + const result: boolean = helperGeoService.inRadius( + { + longitude: 6.1754, + latitude: 106.8272, + radiusInMeters: 10, + }, + { longitude: 6.1754, latitude: 106.8272 } + ); + + jest.spyOn(helperGeoService, 'inRadius').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); +}); diff --git a/test/unit/helper/helper.hash.service.spec.ts b/test/unit/helper/helper.hash.service.spec.ts new file mode 100644 index 0000000..270b8ef --- /dev/null +++ b/test/unit/helper/helper.hash.service.spec.ts @@ -0,0 +1,129 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { HelperHashService } from 'src/common/helper/services/helper.hash.service'; +import configs from 'src/configs'; + +describe('HelperHashService', () => { + let helperHashService: HelperHashService; + const data = 'aaaa'; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperHashService = moduleRef.get(HelperHashService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(helperHashService).toBeDefined(); + }); + + describe('randomSalt', () => { + it('should be success', async () => { + const result: string = helperHashService.randomSalt(10); + + jest.spyOn(helperHashService, 'randomSalt').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('bcrypt', () => { + it('should be success', async () => { + const salt = helperHashService.randomSalt(10); + const result: string = helperHashService.bcrypt(data, salt); + + jest.spyOn(helperHashService, 'bcrypt').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.startsWith(salt)).toBe(true); + }); + }); + + describe('bcryptCompare', () => { + it('should be success', async () => { + const salt = helperHashService.randomSalt(10); + const hash = helperHashService.bcrypt(data, salt); + const result: boolean = helperHashService.bcryptCompare(data, hash); + + jest.spyOn(helperHashService, 'bcryptCompare').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const salt = helperHashService.randomSalt(10); + const hash = helperHashService.bcrypt(data, salt); + const result: boolean = helperHashService.bcryptCompare( + 'bbbb', + hash + ); + + jest.spyOn(helperHashService, 'bcryptCompare').mockReturnValueOnce( + result + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('sha256', () => { + it('should be success', async () => { + const result: string = helperHashService.sha256(data); + + jest.spyOn(helperHashService, 'sha256').mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('sha256Compare', () => { + it('should be success', async () => { + const hash = helperHashService.sha256(data); + const result: boolean = helperHashService.sha256Compare(hash, hash); + + jest.spyOn(helperHashService, 'bcryptCompare').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const hash = helperHashService.sha256(data); + const result: boolean = helperHashService.sha256Compare( + 'bbbb', + hash + ); + + jest.spyOn(helperHashService, 'bcryptCompare').mockReturnValueOnce( + result + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); +}); diff --git a/test/unit/helper/helper.number.service.spec.ts b/test/unit/helper/helper.number.service.spec.ts new file mode 100644 index 0000000..92a5a17 --- /dev/null +++ b/test/unit/helper/helper.number.service.spec.ts @@ -0,0 +1,113 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { HelperNumberService } from 'src/common/helper/services/helper.number.service'; +import configs from 'src/configs'; + +describe('HelperNumberService', () => { + let helperNumberService: HelperNumberService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperNumberService = + moduleRef.get(HelperNumberService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(helperNumberService).toBeDefined(); + }); + + describe('check', () => { + it('should be success', async () => { + const result: boolean = helperNumberService.check('111'); + + jest.spyOn(helperNumberService, 'check').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('create', () => { + it('should be success', async () => { + const result: number = helperNumberService.create('111'); + + jest.spyOn(helperNumberService, 'create').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(111); + }); + }); + + describe('random', () => { + it('should be success', async () => { + const result: number = helperNumberService.random(10); + + jest.spyOn(helperNumberService, 'random').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('randomInRange', () => { + it('should be success', async () => { + const result: number = helperNumberService.randomInRange(5, 8); + + jest.spyOn( + helperNumberService, + 'randomInRange' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + }); + + describe('percent', () => { + it('should be called', async () => { + const test = jest.spyOn(helperNumberService, 'percent'); + + helperNumberService.percent(5, 8); + expect(test).toHaveBeenCalledWith(5, 8); + }); + + it('should be success with NaN or Infinite', async () => { + const result = helperNumberService.percent(0, 0); + jest.spyOn(helperNumberService, 'percent').mockImplementation( + () => result + ); + + expect(helperNumberService.percent(5, 8)).toBe(result); + }); + + it('should be success', async () => { + const result = helperNumberService.percent(5, 8); + jest.spyOn(helperNumberService, 'percent').mockImplementation( + () => result + ); + + expect(helperNumberService.percent(5, 8)).toBe(result); + }); + }); +}); diff --git a/test/unit/helper/helper.string.service.spec.ts b/test/unit/helper/helper.string.service.spec.ts new file mode 100644 index 0000000..54a56ae --- /dev/null +++ b/test/unit/helper/helper.string.service.spec.ts @@ -0,0 +1,229 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { HelperStringService } from 'src/common/helper/services/helper.string.service'; +import configs from 'src/configs'; + +describe('HelperStringService', () => { + let helperStringService: HelperStringService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + ], + }).compile(); + + helperStringService = + moduleRef.get(HelperStringService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(helperStringService).toBeDefined(); + }); + + describe('checkEmail', () => { + it('should be success', async () => { + const result: boolean = + helperStringService.checkEmail('111@mail.com'); + + jest.spyOn(helperStringService, 'checkEmail').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be failed', async () => { + const result: boolean = helperStringService.checkEmail('111'); + + jest.spyOn(helperStringService, 'checkEmail').mockReturnValueOnce( + result + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); + + describe('randomReference', () => { + it('should be success', async () => { + const result: string = helperStringService.randomReference(10); + + jest.spyOn( + helperStringService, + 'randomReference' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + }); + + it('should be success with prefix', async () => { + const result: string = helperStringService.randomReference( + 10, + 'test' + ); + + jest.spyOn( + helperStringService, + 'randomReference' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result.startsWith('test')).toBe(true); + }); + }); + + describe('random', () => { + it('should be success', async () => { + const result = helperStringService.random(5); + + jest.spyOn(helperStringService, 'random').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('should be success with options prefix', async () => { + const result = helperStringService.random(5, { prefix: 'aaa' }); + + jest.spyOn(helperStringService, 'random').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result.startsWith('aaa')).toBe(true); + }); + + it('should be success with options prefix, safe, and uppercase', async () => { + const result = helperStringService.random(5, { + prefix: 'aaa', + safe: true, + upperCase: true, + }); + + jest.spyOn(helperStringService, 'random').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + expect(result.startsWith('AAA')).toBe(true); + }); + }); + + describe('censor', () => { + it('string length 1 should be success', async () => { + const result: string = helperStringService.censor('1'); + + jest.spyOn(helperStringService, 'censor').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('string length 1 - 4 should be success', async () => { + const result: string = helperStringService.censor('125'); + + jest.spyOn(helperStringService, 'censor').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('string length 4 - 10 should be success', async () => { + const result: string = helperStringService.censor('123245'); + + jest.spyOn(helperStringService, 'censor').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + + it('string length > 10 should be success', async () => { + const result: string = helperStringService.censor( + '12312312312312312312312' + ); + + jest.spyOn(helperStringService, 'censor').mockReturnValueOnce( + result + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('checkPasswordWeak', () => { + it('should be success', async () => { + const result: boolean = + helperStringService.checkPasswordWeak('aaAAbbBBccCC'); + + jest.spyOn( + helperStringService, + 'checkPasswordWeak' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('checkPasswordMedium', () => { + it('should be success', async () => { + const result: boolean = + helperStringService.checkPasswordMedium('aaAA12345'); + + jest.spyOn( + helperStringService, + 'checkPasswordMedium' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('checkPasswordStrong', () => { + it('should be success', async () => { + const result: boolean = + helperStringService.checkPasswordStrong('aaAA12345@!'); + + jest.spyOn( + helperStringService, + 'checkPasswordStrong' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('checkSafeString', () => { + it('should be success', async () => { + const result: boolean = helperStringService.checkSafeString('123'); + + jest.spyOn( + helperStringService, + 'checkSafeString' + ).mockReturnValueOnce(result); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); +}); diff --git a/test/unit/jest.json b/test/unit/jest.json new file mode 100644 index 0000000..3f155c6 --- /dev/null +++ b/test/unit/jest.json @@ -0,0 +1,49 @@ +{ + "testTimeout": 10000, + "rootDir": "../../", + "modulePaths": [ + "." + ], + "testEnvironment": "node", + "testMatch": [ + "/test/unit/api-key/*.spec.ts", + "/test/unit/auth/*.spec.ts", + "/test/unit/config/*.spec.ts", + "/test/unit/database/*.spec.ts", + "/test/unit/debugger/*.spec.ts", + "/test/unit/helper/*.spec.ts", + "/test/unit/logger/*.spec.ts", + "/test/unit/message/*.spec.ts", + "/test/unit/pagination/*.spec.ts", + "/test/unit/setting/*.spec.ts" + ], + "collectCoverage": true, + "coverageDirectory": "coverage", + "collectCoverageFrom": [ + "./src/common/api-key/services/**", + "./src/common/auth/services/**", + "./src/common/database/services/**", + "./src/common/debugger/services/**", + "./src/common/helper/services/**", + "./src/common/logger/services/**", + "./src/common/message/services/**", + "./src/common/pagination/services/**", + "./src/common/setting/services/**" + ], + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": 100 + } + }, + "moduleFileExtensions": [ + "js", + "ts", + "json" + ], + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/test/unit/logger/logger.service.spec.ts b/test/unit/logger/logger.service.spec.ts new file mode 100644 index 0000000..dff4add --- /dev/null +++ b/test/unit/logger/logger.service.spec.ts @@ -0,0 +1,249 @@ +import { faker } from '@faker-js/faker'; +import { ConfigModule } from '@nestjs/config'; +import { MongooseModule } from '@nestjs/mongoose'; +import { Test } from '@nestjs/testing'; +import { ApiKeyModule } from 'src/common/api-key/api-key.module'; +import { IApiKeyCreated } from 'src/common/api-key/interfaces/api-key.interface'; +import { ApiKeyService } from 'src/common/api-key/services/api-key.service'; +import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant'; +import { DatabaseOptionsModule } from 'src/common/database/database.options.module'; +import { DatabaseOptionsService } from 'src/common/database/services/database.options.service'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { + ENUM_LOGGER_ACTION, + ENUM_LOGGER_LEVEL, +} from 'src/common/logger/constants/logger.enum.constant'; +import { LoggerCreateDto } from 'src/common/logger/dtos/logger.create.dto'; +import { LoggerModule } from 'src/common/logger/logger.module'; +import { LoggerEntity } from 'src/common/logger/repository/entities/logger.entity'; +import { LoggerService } from 'src/common/logger/services/logger.service'; +import { ENUM_REQUEST_METHOD } from 'src/common/request/constants/request.enum.constant'; +import configs from 'src/configs'; + +describe('LoggerService', () => { + let apiKeyService: ApiKeyService; + let loggerService: LoggerService; + + let apiKey: IApiKeyCreated; + + const loggerLevel: ENUM_LOGGER_LEVEL = ENUM_LOGGER_LEVEL.INFO; + const logger: LoggerCreateDto = { + action: ENUM_LOGGER_ACTION.TEST, + description: 'test aaa', + method: ENUM_REQUEST_METHOD.GET, + tags: [], + path: '/path', + }; + + const loggerComplete: LoggerCreateDto = { + action: ENUM_LOGGER_ACTION.TEST, + description: 'test aaa', + user: DatabaseDefaultUUID(), + apiKey: DatabaseDefaultUUID(), + requestId: DatabaseDefaultUUID(), + role: DatabaseDefaultUUID(), + accessFor: ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN, + method: ENUM_REQUEST_METHOD.GET, + statusCode: 10000, + bodies: { + test: 'aaa', + }, + params: { + test: 'bbb', + }, + path: '/path-complete', + tags: [], + }; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + MongooseModule.forRootAsync({ + connectionName: DATABASE_CONNECTION_NAME, + imports: [DatabaseOptionsModule], + inject: [DatabaseOptionsService], + useFactory: ( + databaseOptionsService: DatabaseOptionsService + ) => databaseOptionsService.createOptions(), + }), + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + LoggerModule, + ApiKeyModule, + ], + }).compile(); + + loggerService = moduleRef.get(LoggerService); + apiKeyService = moduleRef.get(ApiKeyService); + + const apiKeyCreate = { + name: faker.internet.userName(), + description: faker.random.alphaNumeric(), + }; + apiKey = await apiKeyService.create(apiKeyCreate); + + loggerComplete.apiKey = apiKey.doc._id; + }); + + afterEach(async () => { + jest.clearAllMocks(); + + try { + await apiKeyService.deleteMany({ _id: apiKey.doc._id }); + } catch (err: any) { + console.error(err); + } + }); + + it('should be defined', () => { + expect(loggerService).toBeDefined(); + }); + + describe('info', () => { + it('should be success', async () => { + const result: LoggerEntity = await loggerService.info(logger); + + jest.spyOn(loggerService, 'info').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.INFO); + }); + + it('with complete data', async () => { + const result: LoggerEntity = await loggerService.info( + loggerComplete + ); + + jest.spyOn(loggerService, 'info').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.INFO); + }); + }); + + describe('debug', () => { + it('should be success', async () => { + const result: LoggerEntity = await loggerService.debug(logger); + + jest.spyOn(loggerService, 'debug').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.DEBUG); + }); + + it('with complete data', async () => { + const result: LoggerEntity = await loggerService.debug( + loggerComplete + ); + + jest.spyOn(loggerService, 'debug').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.DEBUG); + }); + }); + + describe('warning', () => { + it('should be success', async () => { + const result: LoggerEntity = await loggerService.warn(logger); + + jest.spyOn(loggerService, 'warn').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.WARM); + }); + + it('with complete data', async () => { + const result: LoggerEntity = await loggerService.warn( + loggerComplete + ); + + jest.spyOn(loggerService, 'warn').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.WARM); + }); + }); + + describe('fatal', () => { + it('should be success', async () => { + const result: LoggerEntity = await loggerService.fatal(logger); + + jest.spyOn(loggerService, 'fatal').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.FATAL); + }); + + it('with complete data', async () => { + const result: LoggerEntity = await loggerService.fatal( + loggerComplete + ); + + jest.spyOn(loggerService, 'fatal').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(ENUM_LOGGER_LEVEL.FATAL); + }); + }); + + describe('raw', () => { + it('should be success', async () => { + const result: LoggerEntity = await loggerService.raw({ + level: loggerLevel, + ...logger, + }); + + jest.spyOn(loggerService, 'raw').mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(loggerLevel); + }); + + it('should be success complete', async () => { + const result: LoggerEntity = await loggerService.raw({ + level: loggerLevel, + ...loggerComplete, + }); + + jest.spyOn(loggerService, 'raw').mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result._id).toBeDefined(); + expect(result.level).toBe(loggerLevel); + }); + }); +}); diff --git a/test/unit/message/message.service.spec.ts b/test/unit/message/message.service.spec.ts new file mode 100644 index 0000000..46ff1cb --- /dev/null +++ b/test/unit/message/message.service.spec.ts @@ -0,0 +1,481 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { ValidationError } from 'class-validator'; +import { + IErrors, + IErrorsImport, + IValidationErrorImport, +} from 'src/common/error/interfaces/error.interface'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { IMessage } from 'src/common/message/interfaces/message.interface'; +import { MessageModule } from 'src/common/message/message.module'; +import { MessageService } from 'src/common/message/services/message.service'; +import configs from 'src/configs'; + +describe('MessageService', () => { + let messageService: MessageService; + + let validationError: ValidationError[]; + let validationErrorTwo: ValidationError[]; + let validationErrorThree: ValidationError[]; + let validationErrorConstrainEmpty: ValidationError[]; + let validationErrorImport: IValidationErrorImport[]; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + MessageModule, + ], + }).compile(); + + messageService = moduleRef.get(MessageService); + + validationError = [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + children: [], + constraints: { isEmail: 'email must be an email' }, + }, + ]; + + validationErrorTwo = [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + constraints: { isEmail: 'email must be an email' }, + children: [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + constraints: { + isEmail: 'email must be an email', + }, + children: [], + }, + ], + }, + ]; + + validationErrorThree = [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + constraints: { isEmail: 'email must be an email' }, + children: [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + constraints: { + isEmail: 'email must be an email', + }, + children: [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + constraints: { + isEmail: 'email must be an email', + }, + children: [], + }, + ], + }, + ], + }, + ]; + + validationErrorConstrainEmpty = [ + { + target: { + email: 'admin-mail.com', + password: 'aaAA@@123444', + rememberMe: true, + }, + value: 'admin-mail.com', + property: 'email', + children: [], + }, + ]; + + validationErrorImport = [ + { + row: 0, + file: 'error.xlsx', + errors: [ + { + target: { + number: 1, + area: 'area', + city: 'area timur', + gps: { latitude: 6.1754, longitude: 106.8272 }, + address: 'address 1', + tags: ['test', 'lala'], + }, + property: 'mainBranch', + children: [], + constraints: { + isNotEmpty: 'mainBranch should not be empty', + isString: 'mainBranch must be a string', + }, + }, + ], + }, + { + row: 1, + file: 'error.xlsx', + errors: [ + { + target: { + number: 2, + area: 'area', + city: 'area timur', + tags: [], + }, + property: 'mainBranch', + children: [], + constraints: { + isNotEmpty: 'mainBranch should not be empty', + isString: 'mainBranch must be a string', + }, + }, + ], + }, + { + row: 2, + file: 'error.xlsx', + errors: [ + { + target: { + number: null, + area: 'area', + city: 'area timur', + address: 'address 3', + tags: ['test'], + }, + value: null, + property: 'number', + children: [], + constraints: { + min: 'number must not be less than 0', + isNumber: + 'number must be a number conforming to the specified constraints', + }, + }, + { + target: { + number: null, + area: 'area', + city: 'area timur', + address: 'address 3', + tags: ['test'], + }, + property: 'mainBranch', + children: [], + constraints: { + isNotEmpty: 'mainBranch should not be empty', + isString: 'mainBranch must be a string', + }, + }, + ], + }, + { + row: 3, + file: 'error.xlsx', + errors: [ + { + target: { + number: 4, + area: 'area', + city: 'area timur', + gps: { latitude: 6.1754, longitude: 106.8273 }, + address: 'address 4', + tags: ['hand', 'test'], + }, + property: 'mainBranch', + children: [], + constraints: { + isNotEmpty: 'mainBranch should not be empty', + isString: 'mainBranch must be a string', + }, + }, + ], + }, + { + row: 4, + file: 'error.xlsx', + errors: [ + { + target: { + number: null, + area: 'area', + city: 'area timur', + tags: ['lala'], + }, + value: null, + property: 'number', + children: [], + constraints: { + min: 'number must not be less than 0', + isNumber: + 'number must be a number conforming to the specified constraints', + }, + }, + { + target: { + number: null, + area: 'area', + city: 'area timur', + tags: ['lala'], + }, + property: 'mainBranch', + children: [], + constraints: { + isNotEmpty: 'mainBranch should not be empty', + isString: 'mainBranch must be a string', + }, + }, + ], + }, + { + row: 5, + file: 'error.xlsx', + errors: [ + { + target: { + number: 6, + area: 'area', + city: 'area timur', + gps: { latitude: 6.1754, longitude: 106.8273 }, + address: 'address 6', + tags: [], + }, + property: 'mainBranch', + children: [], + constraints: { + isNotEmpty: 'mainBranch should not be empty', + isString: 'mainBranch must be a string', + }, + }, + ], + }, + ]; + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(messageService).toBeDefined(); + }); + + describe('getAvailableLanguages', () => { + it('should be success', async () => { + const result: string[] = + await messageService.getAvailableLanguages(); + + jest.spyOn( + messageService, + 'getAvailableLanguages' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + expect(result).toBeTruthy(); + }); + }); + + + describe('get', () => { + it('should be success', async () => { + const result: string | IMessage = await messageService.get( + 'test.hello' + ); + + jest.spyOn(messageService, 'get').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('getRequestErrorsMessage', () => { + it('single message should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage(validationError, [ + 'en', + ]); + + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('multi message should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage(validationError, [ + 'en', + 'id', + ]); + + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('multi message if there has some undefined value should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage(validationError, [ + undefined, + 'id', + ]); + + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage(validationError); + + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage(validationError); + + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('two children should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage( + validationErrorTwo + ); + + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('three children should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage( + validationErrorThree + ); + + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('empty constrain should be success', async () => { + const result: IErrors[] = + await messageService.getRequestErrorsMessage( + validationErrorConstrainEmpty + ); + jest.spyOn( + messageService, + 'getRequestErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + }); + + describe('getImportErrorsMessage', () => { + it('should be success', async () => { + const result: IErrorsImport[] = + await messageService.getImportErrorsMessage( + validationErrorImport + ); + + jest.spyOn( + messageService, + 'getImportErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + + it('should be success with options', async () => { + const result: IErrorsImport[] = + await messageService.getImportErrorsMessage( + validationErrorImport, + ['en', 'id'] + ); + + jest.spyOn( + messageService, + 'getImportErrorsMessage' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + }); +}); diff --git a/test/unit/pagination/pagination.service.spec.ts b/test/unit/pagination/pagination.service.spec.ts new file mode 100644 index 0000000..28868c2 --- /dev/null +++ b/test/unit/pagination/pagination.service.spec.ts @@ -0,0 +1,423 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { + PAGINATION_AVAILABLE_ORDER_BY, + PAGINATION_MAX_PAGE, + PAGINATION_MAX_PER_PAGE, + PAGINATION_ORDER_BY, + PAGINATION_ORDER_DIRECTION, + PAGINATION_PAGE, + PAGINATION_PER_PAGE, +} from 'src/common/pagination/constants/pagination.constant'; +import { IPaginationOrder } from 'src/common/pagination/interfaces/pagination.interface'; +import { PaginationModule } from 'src/common/pagination/pagination.module'; +import { PaginationService } from 'src/common/pagination/services/pagination.service'; +import configs from 'src/configs'; + +describe('PaginationService', () => { + let paginationService: PaginationService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + PaginationModule, + ], + }).compile(); + + paginationService = moduleRef.get(PaginationService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(paginationService).toBeDefined(); + }); + + describe('offset', () => { + it('should be offset for page 1', async () => { + const result: number = paginationService.offset(1, 10); + + jest.spyOn(paginationService, 'offset').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(0); + }); + + it('should be offset for page 2', async () => { + const result: number = paginationService.offset(2, 10); + + jest.spyOn(paginationService, 'offset').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(10); + }); + + it('should be offset for page 100', async () => { + const result: number = paginationService.offset(10, 10); + + jest.spyOn(paginationService, 'offset').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(90); + }); + + it('should be offset with max perPage', async () => { + const result: number = paginationService.offset(2, 150); + + jest.spyOn(paginationService, 'offset').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(100); + }); + + it('should be offset with max page', async () => { + const result: number = paginationService.offset(50, 10); + + jest.spyOn(paginationService, 'offset').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(190); + }); + }); + + describe('totalPage', () => { + it('should be return a total page base on perPage and total data', async () => { + const result: number = paginationService.totalPage(100, 10); + + jest.spyOn(paginationService, 'totalPage').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(10); + }); + + it('should be return a 1 because total data is zero', async () => { + const result: number = paginationService.totalPage(0, 10); + + jest.spyOn(paginationService, 'totalPage').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(1); + }); + + it('should be return a max total page ', async () => { + const result: number = paginationService.totalPage(10000, 10); + + jest.spyOn(paginationService, 'totalPage').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + expect(result).toBe(20); + }); + }); + + describe('offsetWithoutMax', () => { + it('should be offset for page 1', async () => { + const result: number = paginationService.offsetWithoutMax(1, 10); + + jest.spyOn( + paginationService, + 'offsetWithoutMax' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBe(0); + }); + + it('should be return offset without depends on max page', async () => { + const result: number = paginationService.offsetWithoutMax(50, 10); + + jest.spyOn( + paginationService, + 'offsetWithoutMax' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBe(490); + }); + + it('should be return offset without depends on max per page', async () => { + const result: number = paginationService.offsetWithoutMax(2, 200); + + jest.spyOn( + paginationService, + 'offsetWithoutMax' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBe(200); + }); + }); + + describe('totalPageWithoutMax', () => { + it('should be return a total page base on perPage and total data', async () => { + const result: number = paginationService.totalPageWithoutMax( + 100, + 10 + ); + + jest.spyOn( + paginationService, + 'totalPageWithoutMax' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBe(10); + }); + + it('should be return a 1 because total data is zero', async () => { + const result: number = paginationService.totalPageWithoutMax(0, 10); + + jest.spyOn( + paginationService, + 'totalPageWithoutMax' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBe(1); + }); + + it('should be return a total page without depends on max page ', async () => { + const result: number = paginationService.totalPageWithoutMax( + 10000, + 10 + ); + + jest.spyOn( + paginationService, + 'totalPageWithoutMax' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + expect(result).toBe(1000); + }); + }); + + describe('page', () => { + it('should be return page', async () => { + const result: number = paginationService.page(1); + + jest.spyOn(paginationService, 'page').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(1); + }); + + it('page 0, should convert to default perPage', async () => { + const result: number = paginationService.page(0); + + jest.spyOn(paginationService, 'page').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(PAGINATION_PAGE); + }); + + it('page more than max, should return max page', async () => { + const result: number = paginationService.page( + PAGINATION_MAX_PAGE + 10 + ); + + jest.spyOn(paginationService, 'page').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(PAGINATION_MAX_PAGE); + }); + }); + + describe('perPage', () => { + it('should be return perPage', async () => { + const result: number = paginationService.perPage(1); + + jest.spyOn(paginationService, 'perPage').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(1); + }); + + it('perPage 0, should convert to default perPage', async () => { + const result: number = paginationService.perPage(0); + + jest.spyOn(paginationService, 'perPage').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(PAGINATION_PER_PAGE); + }); + + it('perPage more than max, should return max perPage', async () => { + const result: number = paginationService.perPage( + PAGINATION_MAX_PER_PAGE + 10 + ); + + jest.spyOn(paginationService, 'perPage').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(PAGINATION_MAX_PER_PAGE); + }); + }); + + describe('order', () => { + it('should be return order with null parameter', async () => { + const result: IPaginationOrder = paginationService.order(); + + jest.spyOn(paginationService, 'order').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + + it('should be return order with unallow order by value', async () => { + const result: IPaginationOrder = paginationService.order('status'); + + jest.spyOn(paginationService, 'order').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + + it('should be return order', async () => { + const result: IPaginationOrder = paginationService.order( + PAGINATION_ORDER_BY, + PAGINATION_ORDER_DIRECTION, + PAGINATION_AVAILABLE_ORDER_BY + ); + + jest.spyOn(paginationService, 'order').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('search', () => { + it('should be return search', async () => { + const result: Record = + paginationService.search('test', ['name']); + + jest.spyOn(paginationService, 'search').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + + it('should be return undefined', async () => { + const result: Record = + paginationService.search(undefined, ['name']); + + jest.spyOn(paginationService, 'search').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + }); + }); + + describe('filterEqual', () => { + it('should be return a value', async () => { + const result: Record = + paginationService.filterEqual('name', 'test'); + + jest.spyOn(paginationService, 'filterEqual').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result['name']).toBe('test'); + }); + }); + + describe('filterContain', () => { + it('should be return a value', async () => { + const result: Record = + paginationService.filterContain('name', 'test'); + + jest.spyOn(paginationService, 'filterContain').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('filterContainFullMatch', () => { + it('should be return a value', async () => { + const result: Record = + paginationService.filterContainFullMatch('name', 'test'); + + jest.spyOn( + paginationService, + 'filterContainFullMatch' + ).mockReturnValueOnce(result as any); + + expect(result).toBeTruthy(); + }); + }); + + describe('filterIn', () => { + it('should be return a value', async () => { + const result: Record = + paginationService.filterIn('name', ['test']); + + jest.spyOn(paginationService, 'filterIn').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); + + describe('filterDate', () => { + it('should be return a value', async () => { + const result: Record = paginationService.filterDate( + 'name', + new Date() + ); + + jest.spyOn(paginationService, 'filterDate').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + }); + }); +}); diff --git a/test/unit/setting/setting.service.spec.ts b/test/unit/setting/setting.service.spec.ts new file mode 100644 index 0000000..6c0a581 --- /dev/null +++ b/test/unit/setting/setting.service.spec.ts @@ -0,0 +1,585 @@ +import { faker } from '@faker-js/faker'; +import { ConfigModule } from '@nestjs/config'; +import { MongooseModule } from '@nestjs/mongoose'; +import { Test } from '@nestjs/testing'; +import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant'; +import { DatabaseOptionsModule } from 'src/common/database/database.options.module'; +import { DatabaseOptionsService } from 'src/common/database/services/database.options.service'; +import { HelperModule } from 'src/common/helper/helper.module'; +import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant'; +import { ENUM_SETTING_DATA_TYPE } from 'src/common/setting/constants/setting.enum.constant'; +import { + SettingDoc, + SettingEntity, +} from 'src/common/setting/repository/entities/setting.entity'; +import { SettingService } from 'src/common/setting/services/setting.service'; +import { SettingModule } from 'src/common/setting/setting.module'; +import configs from 'src/configs'; + +describe('SettingService', () => { + let settingService: SettingService; + let setting: SettingDoc; + const settingName1 = `${faker.name.jobArea()}${+new Date()}`; + const settingName2 = `${faker.name.jobArea()}${+new Date()}`; + const settingName3 = `${faker.name.jobArea()}${+new Date()}`; + const settingName4 = `${faker.name.jobArea()}${+new Date()}`; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + MongooseModule.forRootAsync({ + connectionName: DATABASE_CONNECTION_NAME, + imports: [DatabaseOptionsModule], + inject: [DatabaseOptionsService], + useFactory: ( + databaseOptionsService: DatabaseOptionsService + ) => databaseOptionsService.createOptions(), + }), + ConfigModule.forRoot({ + load: configs, + isGlobal: true, + cache: true, + envFilePath: ['.env'], + expandVariables: true, + }), + HelperModule, + SettingModule, + ], + }).compile(); + + settingService = moduleRef.get(SettingService); + + setting = await settingService.create({ + name: `${faker.name.jobArea()}${+new Date()}`, + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + value: 'true', + }); + }); + + afterEach(async () => { + jest.clearAllMocks(); + + try { + await settingService.deleteMany({ _id: setting._id }); + await settingService.deleteMany({ + name: { + $in: [ + settingName1, + settingName2, + settingName3, + settingName4, + ], + }, + }); + } catch (err: any) { + console.error(err); + } + }); + + it('should be defined', () => { + expect(settingService).toBeDefined(); + }); + + describe('findAll', () => { + it('get all setting', async () => { + const result: SettingEntity[] = await settingService.findAll({ + name: setting.name, + }); + + jest.spyOn(settingService, 'findAll').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0]._id).toBe(setting._id); + }); + + it('get all setting with limit and offset', async () => { + const result = await settingService.findAll( + { + name: setting.name, + }, + { paging: { limit: 1, offset: 0 } } + ); + + jest.spyOn(settingService, 'findAll').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0]._id).toBe(setting._id); + }); + + it('get all setting with limit, offset, and sort', async () => { + const result = await settingService.findAll( + { + name: setting.name, + }, + { + paging: { limit: 1, offset: 0 }, + order: { name: ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC }, + } + ); + + jest.spyOn(settingService, 'findAll').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0]._id).toBe(setting._id); + }); + }); + + describe('getTotal', () => { + it('should return a number of total data', async () => { + const result: number = await settingService.getTotal({ + name: setting.name, + }); + + jest.spyOn(settingService, 'getTotal').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(1); + }); + }); + + describe('findOneById', () => { + it('should be success', async () => { + const result: SettingDoc = await settingService.findOneById( + setting._id + ); + + jest.spyOn(settingService, 'findOneById').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(setting._id); + }); + }); + + describe('findOneByName', () => { + it('should be return a setting entity', async () => { + const result: SettingDoc = await settingService.findOneByName( + setting.name + ); + + jest.spyOn(settingService, 'findOneByName').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(setting._id); + }); + }); + + describe('create', () => { + it('should be create a new setting, number', async () => { + const result: SettingDoc = await settingService.create({ + name: settingName1, + type: ENUM_SETTING_DATA_TYPE.NUMBER, + description: 'aaa', + value: '1', + }); + + jest.spyOn(settingService, 'create').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.name).toBe(settingName1); + }); + + it('should be create a new setting, string', async () => { + const result: SettingDoc = await settingService.create({ + name: settingName2, + description: 'test', + type: ENUM_SETTING_DATA_TYPE.STRING, + value: '1', + }); + + jest.spyOn(settingService, 'create').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.name).toBe(settingName2); + }); + + it('should be create a new setting, boolean', async () => { + const result: SettingDoc = await settingService.create({ + name: settingName3, + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + description: 'aaa', + value: 'true', + }); + + jest.spyOn(settingService, 'create').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.name).toBe(settingName3); + }); + + it('should be create a new setting, string', async () => { + const result: SettingDoc = await settingService.create({ + name: settingName4, + description: 'test', + type: ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING, + value: '1,2,3', + }); + + jest.spyOn(settingService, 'create').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result.name).toBe(settingName4); + }); + }); + + describe('updateValue', () => { + it('should be update a value, number', async () => { + const result: SettingDoc = await settingService.updateValue( + setting, + { + value: '1', + type: ENUM_SETTING_DATA_TYPE.NUMBER, + } + ); + + jest.spyOn(settingService, 'updateValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(setting._id); + expect(result.type).toBe(ENUM_SETTING_DATA_TYPE.NUMBER); + expect(result.value).toBe('1'); + }); + + it('should be update a value, string', async () => { + const result: SettingDoc = await settingService.updateValue( + setting, + { + value: 'aaa', + type: ENUM_SETTING_DATA_TYPE.STRING, + } + ); + + jest.spyOn(settingService, 'updateValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(setting._id); + expect(result.type).toBe(ENUM_SETTING_DATA_TYPE.STRING); + expect(result.value).toBe('aaa'); + }); + + it('should be update a value, boolean', async () => { + const result: SettingDoc = await settingService.updateValue( + setting, + { + value: 'true', + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + } + ); + + jest.spyOn(settingService, 'updateValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(setting._id); + expect(result.type).toBe(ENUM_SETTING_DATA_TYPE.BOOLEAN); + expect(result.value).toBe('true'); + }); + + it('should be update a value, array of string', async () => { + const result: SettingDoc = await settingService.updateValue( + setting, + { + value: 'aa,bb,cc', + type: ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING, + } + ); + + jest.spyOn(settingService, 'updateValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(setting._id); + expect(result.type).toBe(ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING); + expect(result.value).toBe('aa,bb,cc'); + }); + }); + + describe('delete', () => { + it('should be success', async () => { + const result: SettingDoc = await settingService.delete(setting); + + jest.spyOn(settingService, 'delete').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result._id).toBe(setting._id); + }); + }); + + describe('getMaintenance', () => { + it('should be return a setting', async () => { + const result: boolean = await settingService.getMaintenance(); + + jest.spyOn(settingService, 'getMaintenance').mockReturnValueOnce( + result as any + ); + + expect(result).toBeDefined(); + }); + }); + + describe('getMobileNumberCountryCodeAllowed', () => { + it('should be return a setting', async () => { + const result: string[] = + await settingService.getMobileNumberCountryCodeAllowed(); + + jest.spyOn( + settingService, + 'getMobileNumberCountryCodeAllowed' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + }); + }); + + describe('getPasswordAttempt', () => { + it('should be return a setting', async () => { + const result: boolean = await settingService.getPasswordAttempt(); + + jest.spyOn( + settingService, + 'getPasswordAttempt' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + }); + }); + + describe('getMaxPasswordAttempt', () => { + it('should be return a setting', async () => { + const result: number = await settingService.getMaxPasswordAttempt(); + + jest.spyOn( + settingService, + 'getMaxPasswordAttempt' + ).mockReturnValueOnce(result as any); + + expect(result).toBeDefined(); + }); + }); + + describe('deleteMany', () => { + it('should be success', async () => { + const result: boolean = await settingService.deleteMany({ + _id: setting._id, + }); + jest.spyOn(settingService, 'deleteMany').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + }); + + describe('getValue', () => { + it('should be return a number value', async () => { + const setting1: SettingDoc = await settingService.create({ + name: settingName1, + value: '1', + type: ENUM_SETTING_DATA_TYPE.NUMBER, + }); + const result: number = await settingService.getValue( + setting1 + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('number'); + }); + + it('should be return a string value', async () => { + const setting2: SettingDoc = await settingService.create({ + name: settingName2, + value: 'aaa', + type: ENUM_SETTING_DATA_TYPE.STRING, + }); + const result: string = await settingService.getValue( + setting2 + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + }); + + it('should be return a boolean value true', async () => { + const setting3: SettingDoc = await settingService.create({ + name: settingName3, + value: 'true', + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + }); + const result: boolean = await settingService.getValue( + setting3 + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('boolean'); + }); + + it('should be return a boolean value false', async () => { + const setting3: SettingDoc = await settingService.create({ + name: settingName3, + value: 'false', + type: ENUM_SETTING_DATA_TYPE.BOOLEAN, + }); + const result: boolean = await settingService.getValue( + setting3 + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + expect(typeof result).toBe('boolean'); + }); + + it('should be return a array of string value', async () => { + const setting4: SettingDoc = await settingService.create({ + name: settingName4, + value: '1,2,3', + type: ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING, + }); + const result: string[] = await settingService.getValue( + setting4 + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(Array.isArray(result)).toBe(true); + expect(typeof result[0]).toBe('string'); + }); + }); + + describe('checkValue', () => { + it('should be check a number value', async () => { + const result: boolean = await settingService.checkValue( + '1', + ENUM_SETTING_DATA_TYPE.NUMBER + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be check a string value', async () => { + const result: boolean = await settingService.checkValue( + 'aaaa', + ENUM_SETTING_DATA_TYPE.STRING + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be check a boolean true value', async () => { + const result: boolean = await settingService.checkValue( + 'true', + ENUM_SETTING_DATA_TYPE.BOOLEAN + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be check a boolean false value', async () => { + const result: boolean = await settingService.checkValue( + 'false', + ENUM_SETTING_DATA_TYPE.BOOLEAN + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be check a array of string value', async () => { + const result: boolean = await settingService.checkValue( + '1,2,3', + ENUM_SETTING_DATA_TYPE.ARRAY_OF_STRING + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeTruthy(); + expect(result).toBe(true); + }); + + it('should be check error', async () => { + const result: boolean = await settingService.checkValue( + 'trueaaa', + ENUM_SETTING_DATA_TYPE.BOOLEAN + ); + + jest.spyOn(settingService, 'getValue').mockReturnValueOnce( + result as any + ); + + expect(result).toBeFalsy(); + expect(result).toBe(false); + }); + }); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..359a2f4 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "node_modules", + "dist", + "*coverage", + "logs", + "scripts", + "test" + ], + "include": [ + "src" + ], +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e0984c3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "useDefineForClassFields": false, + "target": "ESNext", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "allowJs": false, + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true + }, + "include": [ + "src", + "test", + ], + "exclude": [ + "node_modules", + "dist", + "*coverage", + "logs", + "scripts", + ] +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..c3126da --- /dev/null +++ b/yarn.lock @@ -0,0 +1,8140 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + dependencies: + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@angular-devkit/core@14.0.5": + version "14.0.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-14.0.5.tgz#19f5940b53aeb0ce56479c44670d3bc3b2df92b1" + integrity sha512-/CUGi6QLwh79FvsOY7M+1LQL3asZsbQW/WBd5f1iu5y7TLNqCwo+wOb0ZXLDNPw45vYBxFajtt3ob3U7qx3jNg== + dependencies: + ajv "8.11.0" + ajv-formats "2.1.1" + jsonc-parser "3.0.0" + rxjs "6.6.7" + source-map "0.7.3" + +"@angular-devkit/core@15.0.4": + version "15.0.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-15.0.4.tgz#257ba1d76cd106216d0150f480d0062e726af996" + integrity sha512-4ITpRAevd652SxB+qNesIQ9qfbm7wT5UBU5kJOPPwGL77I21g8CQpkmV1n5VSacPvC9Zbz90feOWexf7w7JzcA== + dependencies: + ajv "8.11.0" + ajv-formats "2.1.1" + jsonc-parser "3.2.0" + rxjs "6.6.7" + source-map "0.7.4" + +"@angular-devkit/core@15.1.4": + version "15.1.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-15.1.4.tgz#462f123d56f9298cb04b3fa31b425fc31abb76c5" + integrity sha512-PW5MRmd9DHJR4FaXchwQtj9pXnsghSTnwRvfZeCRNYgU2sv0DKyTV+YTSJB+kNXnoPNG1Je6amDEkiXecpspXg== + dependencies: + ajv "8.12.0" + ajv-formats "2.1.1" + jsonc-parser "3.2.0" + rxjs "6.6.7" + source-map "0.7.4" + +"@angular-devkit/schematics-cli@15.1.4": + version "15.1.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics-cli/-/schematics-cli-15.1.4.tgz#f2ea0379e27ddd6b05b302dd88b8d3f1b6c49ec8" + integrity sha512-qkM5Mfs28jZzNcJnSM6RlyrKkYvzhQmWFTxBXnn15k5T4EnSs1gI6O054Xn7jo/senfwNNt7h2Mlz2OmBLo6+w== + dependencies: + "@angular-devkit/core" "15.1.4" + "@angular-devkit/schematics" "15.1.4" + ansi-colors "4.1.3" + inquirer "8.2.4" + symbol-observable "4.0.0" + yargs-parser "21.1.1" + +"@angular-devkit/schematics@14.0.5": + version "14.0.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-14.0.5.tgz#01777d2ad473d35bdfdbbb751521c43421ad9772" + integrity sha512-sufxITBkn2MvgEREt9JQ3QCKHS+sue1WsVzLE+TWqG5MC/RPk0f9tQ5VoHk6ZTzDKUvOtSoc7G+n0RscQsyp5g== + dependencies: + "@angular-devkit/core" "14.0.5" + jsonc-parser "3.0.0" + magic-string "0.26.1" + ora "5.4.1" + rxjs "6.6.7" + +"@angular-devkit/schematics@15.0.4": + version "15.0.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-15.0.4.tgz#64de42f9100d7080bc3c59bb06d1e4f6f15a088e" + integrity sha512-/gXiLFS0+xFdx6wPoBpe/c6/K9I5edMpaASqPf4XheKtrsSvL+qTlIi3nsbfItzOiDXbaBmlbxGfkMHz/yg0Ig== + dependencies: + "@angular-devkit/core" "15.0.4" + jsonc-parser "3.2.0" + magic-string "0.26.7" + ora "5.4.1" + rxjs "6.6.7" + +"@angular-devkit/schematics@15.1.4": + version "15.1.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-15.1.4.tgz#30e38777f1bd98e20e6dbe1bfddabc3bcd42605f" + integrity sha512-jpddxo9Qd2yRQ1t9FLhAx5S+luz6HkyhDytq0LFKbxf9ikf1J4oy9riPBFl4pRmrNARWcHZ6GbD20/Ky8PjmXQ== + dependencies: + "@angular-devkit/core" "15.1.4" + jsonc-parser "3.2.0" + magic-string "0.27.0" + ora "5.4.1" + rxjs "6.6.7" + +"@aws-crypto/crc32@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa" + integrity sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA== + dependencies: + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + tslib "^1.11.1" + +"@aws-crypto/crc32c@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32c/-/crc32c-3.0.0.tgz#016c92da559ef638a84a245eecb75c3e97cb664f" + integrity sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w== + dependencies: + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + tslib "^1.11.1" + +"@aws-crypto/ie11-detection@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz#640ae66b4ec3395cee6a8e94ebcd9f80c24cd688" + integrity sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q== + dependencies: + tslib "^1.11.1" + +"@aws-crypto/sha1-browser@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha1-browser/-/sha1-browser-3.0.0.tgz#f9083c00782b24714f528b1a1fef2174002266a3" + integrity sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw== + dependencies: + "@aws-crypto/ie11-detection" "^3.0.0" + "@aws-crypto/supports-web-crypto" "^3.0.0" + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-crypto/sha256-browser@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz#05f160138ab893f1c6ba5be57cfd108f05827766" + integrity sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ== + dependencies: + "@aws-crypto/ie11-detection" "^3.0.0" + "@aws-crypto/sha256-js" "^3.0.0" + "@aws-crypto/supports-web-crypto" "^3.0.0" + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-crypto/sha256-js@3.0.0", "@aws-crypto/sha256-js@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz#f06b84d550d25521e60d2a0e2a90139341e007c2" + integrity sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ== + dependencies: + "@aws-crypto/util" "^3.0.0" + "@aws-sdk/types" "^3.222.0" + tslib "^1.11.1" + +"@aws-crypto/supports-web-crypto@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz#5d1bf825afa8072af2717c3e455f35cda0103ec2" + integrity sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg== + dependencies: + tslib "^1.11.1" + +"@aws-crypto/util@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-3.0.0.tgz#1c7ca90c29293f0883468ad48117937f0fe5bfb0" + integrity sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w== + dependencies: + "@aws-sdk/types" "^3.222.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-sdk/abort-controller@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/abort-controller/-/abort-controller-3.292.0.tgz#37c43fd2ce5bcb158aa62e3a5632045ee8a7e3cc" + integrity sha512-lf+OPptL01kvryIJy7+dvFux5KbJ6OTwLPPEekVKZ2AfEvwcVtOZWFUhyw3PJCBTVncjKB1Kjl3V/eTS3YuPXQ== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/chunked-blob-reader-native@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/chunked-blob-reader-native/-/chunked-blob-reader-native-3.292.0.tgz#143fcedfe0bc583bd089dee0d6247b22f5e0db4d" + integrity sha512-A34sBrnggm9mXPZeeEie4jDv9zHRMS0LSm85VkfrBLuYYsfsw9DxmW59wJkuo6DIm/RK04oH5+lRMt34koBgrw== + dependencies: + "@aws-sdk/util-base64" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/chunked-blob-reader@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-3.292.0.tgz#f3a661cf15c8bacbbc761cca8c8eb13625543eb3" + integrity sha512-ccFPnzBjLbDCmFjTXwhsfD58vtEiAjbor3A9tvnou+3Dj6RrMEGPaTu5tcw3mwWb2zh1K3HFJg6Bmb0no49TRw== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/client-s3@^3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.292.0.tgz#13c63a34992c48e8d5145624a80c97f90ba81655" + integrity sha512-avqj7YCicFdB/jZXvbMhe9b0y/GIdJpIGgxZV/RowuEqVan1rlKoHEKnxmTwt/CPz02byLOPIXQ55yDVP7/FvQ== + dependencies: + "@aws-crypto/sha1-browser" "3.0.0" + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/client-sts" "3.292.0" + "@aws-sdk/config-resolver" "3.292.0" + "@aws-sdk/credential-provider-node" "3.292.0" + "@aws-sdk/eventstream-serde-browser" "3.292.0" + "@aws-sdk/eventstream-serde-config-resolver" "3.292.0" + "@aws-sdk/eventstream-serde-node" "3.292.0" + "@aws-sdk/fetch-http-handler" "3.292.0" + "@aws-sdk/hash-blob-browser" "3.292.0" + "@aws-sdk/hash-node" "3.292.0" + "@aws-sdk/hash-stream-node" "3.292.0" + "@aws-sdk/invalid-dependency" "3.292.0" + "@aws-sdk/md5-js" "3.292.0" + "@aws-sdk/middleware-bucket-endpoint" "3.292.0" + "@aws-sdk/middleware-content-length" "3.292.0" + "@aws-sdk/middleware-endpoint" "3.292.0" + "@aws-sdk/middleware-expect-continue" "3.292.0" + "@aws-sdk/middleware-flexible-checksums" "3.292.0" + "@aws-sdk/middleware-host-header" "3.292.0" + "@aws-sdk/middleware-location-constraint" "3.292.0" + "@aws-sdk/middleware-logger" "3.292.0" + "@aws-sdk/middleware-recursion-detection" "3.292.0" + "@aws-sdk/middleware-retry" "3.292.0" + "@aws-sdk/middleware-sdk-s3" "3.292.0" + "@aws-sdk/middleware-serde" "3.292.0" + "@aws-sdk/middleware-signing" "3.292.0" + "@aws-sdk/middleware-ssec" "3.292.0" + "@aws-sdk/middleware-stack" "3.292.0" + "@aws-sdk/middleware-user-agent" "3.292.0" + "@aws-sdk/node-config-provider" "3.292.0" + "@aws-sdk/node-http-handler" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/signature-v4-multi-region" "3.292.0" + "@aws-sdk/smithy-client" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/url-parser" "3.292.0" + "@aws-sdk/util-base64" "3.292.0" + "@aws-sdk/util-body-length-browser" "3.292.0" + "@aws-sdk/util-body-length-node" "3.292.0" + "@aws-sdk/util-defaults-mode-browser" "3.292.0" + "@aws-sdk/util-defaults-mode-node" "3.292.0" + "@aws-sdk/util-endpoints" "3.292.0" + "@aws-sdk/util-retry" "3.292.0" + "@aws-sdk/util-stream-browser" "3.292.0" + "@aws-sdk/util-stream-node" "3.292.0" + "@aws-sdk/util-user-agent-browser" "3.292.0" + "@aws-sdk/util-user-agent-node" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + "@aws-sdk/util-waiter" "3.292.0" + "@aws-sdk/xml-builder" "3.292.0" + fast-xml-parser "4.1.2" + tslib "^2.3.1" + +"@aws-sdk/client-sso-oidc@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.292.0.tgz#8354f5a9e672dc705e769d489731886d52552b1c" + integrity sha512-KANoinZDvWwCXKrx92V0i8ItovKwW94Ep4vLY+D7ZmuV8IACK0XcIR9HF8eMR4Zqy7DSBAGdvvd318Qy2v1f2Q== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/config-resolver" "3.292.0" + "@aws-sdk/fetch-http-handler" "3.292.0" + "@aws-sdk/hash-node" "3.292.0" + "@aws-sdk/invalid-dependency" "3.292.0" + "@aws-sdk/middleware-content-length" "3.292.0" + "@aws-sdk/middleware-endpoint" "3.292.0" + "@aws-sdk/middleware-host-header" "3.292.0" + "@aws-sdk/middleware-logger" "3.292.0" + "@aws-sdk/middleware-recursion-detection" "3.292.0" + "@aws-sdk/middleware-retry" "3.292.0" + "@aws-sdk/middleware-serde" "3.292.0" + "@aws-sdk/middleware-stack" "3.292.0" + "@aws-sdk/middleware-user-agent" "3.292.0" + "@aws-sdk/node-config-provider" "3.292.0" + "@aws-sdk/node-http-handler" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/smithy-client" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/url-parser" "3.292.0" + "@aws-sdk/util-base64" "3.292.0" + "@aws-sdk/util-body-length-browser" "3.292.0" + "@aws-sdk/util-body-length-node" "3.292.0" + "@aws-sdk/util-defaults-mode-browser" "3.292.0" + "@aws-sdk/util-defaults-mode-node" "3.292.0" + "@aws-sdk/util-endpoints" "3.292.0" + "@aws-sdk/util-retry" "3.292.0" + "@aws-sdk/util-user-agent-browser" "3.292.0" + "@aws-sdk/util-user-agent-node" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/client-sso@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.292.0.tgz#977a6834deb946571423c1af8f306157bfee1903" + integrity sha512-DzBBa72TTgfTllvTbD/7KcRY8bo5ExUv8gHJaedrE7mlZUn/2msk9S41rf+Rcwb0bf7k14Y36aRVwoXwQCKPLg== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/config-resolver" "3.292.0" + "@aws-sdk/fetch-http-handler" "3.292.0" + "@aws-sdk/hash-node" "3.292.0" + "@aws-sdk/invalid-dependency" "3.292.0" + "@aws-sdk/middleware-content-length" "3.292.0" + "@aws-sdk/middleware-endpoint" "3.292.0" + "@aws-sdk/middleware-host-header" "3.292.0" + "@aws-sdk/middleware-logger" "3.292.0" + "@aws-sdk/middleware-recursion-detection" "3.292.0" + "@aws-sdk/middleware-retry" "3.292.0" + "@aws-sdk/middleware-serde" "3.292.0" + "@aws-sdk/middleware-stack" "3.292.0" + "@aws-sdk/middleware-user-agent" "3.292.0" + "@aws-sdk/node-config-provider" "3.292.0" + "@aws-sdk/node-http-handler" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/smithy-client" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/url-parser" "3.292.0" + "@aws-sdk/util-base64" "3.292.0" + "@aws-sdk/util-body-length-browser" "3.292.0" + "@aws-sdk/util-body-length-node" "3.292.0" + "@aws-sdk/util-defaults-mode-browser" "3.292.0" + "@aws-sdk/util-defaults-mode-node" "3.292.0" + "@aws-sdk/util-endpoints" "3.292.0" + "@aws-sdk/util-retry" "3.292.0" + "@aws-sdk/util-user-agent-browser" "3.292.0" + "@aws-sdk/util-user-agent-node" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/client-sts@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.292.0.tgz#7d0c66f761c6d1dec9bd67ed5ae04320753a56dd" + integrity sha512-t9Q0+iT8E1QAARq7aUHdF5KwgXrdW1yl4lsnkmVcLJKypyhnXTVJ68qldV6rBDSFswGqT0SBQBzcAj6vPNlOFQ== + dependencies: + "@aws-crypto/sha256-browser" "3.0.0" + "@aws-crypto/sha256-js" "3.0.0" + "@aws-sdk/config-resolver" "3.292.0" + "@aws-sdk/credential-provider-node" "3.292.0" + "@aws-sdk/fetch-http-handler" "3.292.0" + "@aws-sdk/hash-node" "3.292.0" + "@aws-sdk/invalid-dependency" "3.292.0" + "@aws-sdk/middleware-content-length" "3.292.0" + "@aws-sdk/middleware-endpoint" "3.292.0" + "@aws-sdk/middleware-host-header" "3.292.0" + "@aws-sdk/middleware-logger" "3.292.0" + "@aws-sdk/middleware-recursion-detection" "3.292.0" + "@aws-sdk/middleware-retry" "3.292.0" + "@aws-sdk/middleware-sdk-sts" "3.292.0" + "@aws-sdk/middleware-serde" "3.292.0" + "@aws-sdk/middleware-signing" "3.292.0" + "@aws-sdk/middleware-stack" "3.292.0" + "@aws-sdk/middleware-user-agent" "3.292.0" + "@aws-sdk/node-config-provider" "3.292.0" + "@aws-sdk/node-http-handler" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/smithy-client" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/url-parser" "3.292.0" + "@aws-sdk/util-base64" "3.292.0" + "@aws-sdk/util-body-length-browser" "3.292.0" + "@aws-sdk/util-body-length-node" "3.292.0" + "@aws-sdk/util-defaults-mode-browser" "3.292.0" + "@aws-sdk/util-defaults-mode-node" "3.292.0" + "@aws-sdk/util-endpoints" "3.292.0" + "@aws-sdk/util-retry" "3.292.0" + "@aws-sdk/util-user-agent-browser" "3.292.0" + "@aws-sdk/util-user-agent-node" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + fast-xml-parser "4.1.2" + tslib "^2.3.1" + +"@aws-sdk/config-resolver@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-3.292.0.tgz#c5c9b86a2a75aa591bc7acdbe94557367a2a7d90" + integrity sha512-cB3twnNR7vYvlt2jvw8VlA1+iv/tVzl+/S39MKqw2tepU+AbJAM0EHwb/dkf1OKSmlrnANXhshx80MHF9zL4mA== + dependencies: + "@aws-sdk/signature-v4" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-config-provider" "3.292.0" + "@aws-sdk/util-middleware" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-env@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.292.0.tgz#bde3333b7bee715c8a41113f1c6deb0e896a59da" + integrity sha512-YbafSG0ZEKE2969CJWVtUhh3hfOeLPecFVoXOtegCyAJgY5Ghtu4TsVhL4DgiGAgOC30ojAmUVQEXzd7xJF5xA== + dependencies: + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-imds@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.292.0.tgz#557e59c637c3852cac54534319c75eb015aa3081" + integrity sha512-W/peOgDSRYulgzFpUhvgi1pCm6piBz6xrVN17N4QOy+3NHBXRVMVzYk6ct2qpLPgJUSEZkcpP+Gds+bBm8ed1A== + dependencies: + "@aws-sdk/node-config-provider" "3.292.0" + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/url-parser" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-ini@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.292.0.tgz#1d67ea9f560d084051d63f5695a1ddcede09e0ad" + integrity sha512-gTXSGjx3Q+KY8Zz/XHTDWOBx9UWtL3s8tTdpQOdaMqqm0xIK5X4KDud3L/huPpZYm0a7rNAML8l1mU56FFnBVw== + dependencies: + "@aws-sdk/credential-provider-env" "3.292.0" + "@aws-sdk/credential-provider-imds" "3.292.0" + "@aws-sdk/credential-provider-process" "3.292.0" + "@aws-sdk/credential-provider-sso" "3.292.0" + "@aws-sdk/credential-provider-web-identity" "3.292.0" + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/shared-ini-file-loader" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.292.0.tgz#c2620b28f9f04c01e66378539075f23a4ff6862d" + integrity sha512-85LQIeSGSQtbrgqEYmCcUnehBmTKt8bbn7mN9RxbtCDnZVgEagJCid7o9+fYQXZ5IjXaHLUApoLsv6ytEj4ITA== + dependencies: + "@aws-sdk/credential-provider-env" "3.292.0" + "@aws-sdk/credential-provider-imds" "3.292.0" + "@aws-sdk/credential-provider-ini" "3.292.0" + "@aws-sdk/credential-provider-process" "3.292.0" + "@aws-sdk/credential-provider-sso" "3.292.0" + "@aws-sdk/credential-provider-web-identity" "3.292.0" + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/shared-ini-file-loader" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-process@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.292.0.tgz#52caa9d46d227e02fda5807d32a177a0819eee97" + integrity sha512-CFVXuMuUvg/a4tknzRikEDwZBnKlHs1LZCpTXIGjBdUTdosoi4WNzDLzGp93ZRTtcgFz+4wirz2f7P3lC0NrQw== + dependencies: + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/shared-ini-file-loader" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-sso@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.292.0.tgz#ae01ab2ff4771ebf5302f264200a116a6b9cdc00" + integrity sha512-+jrhi0oZc9dMtbRsqi+lkqIheCb8QlsRJSEKDa3nUlyxaOkzRKR9Yf5Jtpqooa0ichFhMVZTD9oXPFrlGROIEQ== + dependencies: + "@aws-sdk/client-sso" "3.292.0" + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/shared-ini-file-loader" "3.292.0" + "@aws-sdk/token-providers" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-web-identity@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.292.0.tgz#60e180eadd0947891ed041f6a4574fa2074d0d4c" + integrity sha512-4DbtIEM9gGVfqYlMdYXg3XY+vBhemjB1zXIequottW8loLYM8Vuz4/uGxxKNze6evVVzowsA0wKrYclE1aj/Rg== + dependencies: + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-codec@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-codec/-/eventstream-codec-3.292.0.tgz#6c4f34a0f7bf9113bc26f3c736fb0c023cb87e43" + integrity sha512-P0np4vhCKf/JH6I39Id8DxZR+UZzG+Br+vOrTinerMfOhzTa2229XmL8pwlMpOoxnJLMPmEDtD1KQqLslBEXtw== + dependencies: + "@aws-crypto/crc32" "3.0.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-hex-encoding" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-browser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.292.0.tgz#090af8854387056e9674b8688a815b93c8548fd6" + integrity sha512-VzRbJqqE444GOuoNTxTJ1dC1IhNhA6jfHjgsI8iDRHraaEukGqsPx1vkc+byxrDEjgxKN5IqOwZ4yJWMIAozBA== + dependencies: + "@aws-sdk/eventstream-serde-universal" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-config-resolver@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.292.0.tgz#79b1b2194a0386dae6e4134c1534e5ce62a18312" + integrity sha512-Ndx+qJyWmBCW9FSm68AGLoO4AZ0AaL/wjpJEgFF2sZBWjYe9O9PB9IGR/yuqCBTElf3YtSiFMsloikQaz2ft6g== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.292.0.tgz#2001ab936ce316aa713d69beecc133a77300d445" + integrity sha512-NFCEiNCetNye7jQfRd5y/7J9dLg9+uL57698wYeXeadlwJ8Cd/Nhsz+t7RIbP05VqshU+anXARMB1avl9oAijQ== + dependencies: + "@aws-sdk/eventstream-serde-universal" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-universal@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.292.0.tgz#841b0488ce526f40a7f4c69b77af5ea0b7669f12" + integrity sha512-1gqZNx+S1EUpl3Tq6uIesiDx8gnkpXqPsFfCZT7lSWWXBpnHmnUZAh3jbiO9UlQbYuB9SfT0EBKb1iOY9z4j1Q== + dependencies: + "@aws-sdk/eventstream-codec" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/fetch-http-handler@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.292.0.tgz#a99d915e019e888bfdfa3e5da68606bfc4c80522" + integrity sha512-zh3bhUJbL8RSa39ZKDcy+AghtUkIP8LwcNlwRIoxMQh3Row4D1s4fCq0KZCx98NJBEXoiTLyTQlZxxI//BOb1Q== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/querystring-builder" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-base64" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/hash-blob-browser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.292.0.tgz#d62d8556877f0823fdcc3f08dac634389219e62e" + integrity sha512-4+Fm4IOkxGqgx8dU0EbExCq6xx30y369ZSXz89h9YDQYdJ2Muw7iNCHAg/4VM+gfp0vo9J8zPOTsSju8LNS5Jg== + dependencies: + "@aws-sdk/chunked-blob-reader" "3.292.0" + "@aws-sdk/chunked-blob-reader-native" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/hash-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/hash-node/-/hash-node-3.292.0.tgz#4f62e36a7cdefd0f4bca4c1d16261d36a4596442" + integrity sha512-1yLxmIsvE+eK36JXEgEIouTITdykQLVhsA5Oai//Lar6Ddgu1sFpLDbdkMtKbrh4I0jLN9RacNCkeVQjZPTCCQ== + dependencies: + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-buffer-from" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/hash-stream-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/hash-stream-node/-/hash-stream-node-3.292.0.tgz#1a5c17c322bd02f5a14046a10a664c4d96cd06de" + integrity sha512-p2nj9A5lZKQU45Q4Od3iZDvpziEpojAyuyAI0HPzpIuJIfzFQ0/7pMBKde1li6wq93rpyFLwNufV6FEZnKCYRg== + dependencies: + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/invalid-dependency@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/invalid-dependency/-/invalid-dependency-3.292.0.tgz#0e5b47cacf459db6ae8dddc02d613a5bd0ff3555" + integrity sha512-39OUV78CD3TmEbjhpt+V+Fk4wAGWhixqHxDSN8+4WL0uB4Fl7k5m3Z9hNY78AttHQSl2twR7WtLztnXPAFsriw== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/is-array-buffer@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/is-array-buffer/-/is-array-buffer-3.292.0.tgz#d599c7ad4ad104918d52b8d2160091ca5b0a1971" + integrity sha512-kW/G5T/fzI0sJH5foZG6XJiNCevXqKLxV50qIT4B1pMuw7regd4ALIy0HwSqj1nnn9mSbRWBfmby0jWCJsMcwg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/md5-js@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/md5-js/-/md5-js-3.292.0.tgz#740b4e6bbe24a41fefb78f93f436da55e438555d" + integrity sha512-ngfsKLgQenXW3EbsDf47PVNys1SecTbsq6k88h7+Aa8BU49+9ZOIz4VDpWuPiNyYpeV7jJdl1dfD+ujOYvvgNw== + dependencies: + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-bucket-endpoint@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.292.0.tgz#28f7bfc3dfeef43598dee96aa597cadee060a117" + integrity sha512-XRy9RSUIRcbxYfH504ywhQllgfdf3wVhk2k0mMPYnUbeEhAFe1/eUog2v/bi07/q5TQ4Hppi+W3nHCVualQEow== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-arn-parser" "3.292.0" + "@aws-sdk/util-config-provider" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-content-length@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-content-length/-/middleware-content-length-3.292.0.tgz#f2035aee536abf553b743202879ee86171c4c3c7" + integrity sha512-2gMWzQus5mj14menolpPDbYBeaOYcj7KNFZOjTjjI3iQ0KqyetG6XasirNrcJ/8QX1BRmpTol8Xjp2Ue3Gbzwg== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-endpoint@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.292.0.tgz#c6809a2e001ab03cac223dfae48439e893da627b" + integrity sha512-cPMkiSxpZGG6tYlW4OS+ucS6r43f9ddX9kcUoemJCY10MOuogdPjulCAjE0HTs2PLKSOrrG4CTP4Q4wWDrH4Bw== + dependencies: + "@aws-sdk/middleware-serde" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/signature-v4" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/url-parser" "3.292.0" + "@aws-sdk/util-config-provider" "3.292.0" + "@aws-sdk/util-middleware" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-expect-continue@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.292.0.tgz#11edbf77d741ab169a469c918bb4b39dd9604438" + integrity sha512-bZ2bsBud3E6BebZWGxVcWxBSg09bP0KyX8PT0jI66JM0yTbZSJhoGhlKAqfNG46R9h4K5tCYB2uYgV/3oU/ZpQ== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-flexible-checksums@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.292.0.tgz#f9a52631d65ead667457c57bcf435c2fcee72d13" + integrity sha512-AxU/Gb+TRdl/0jHmbreYh3QnB0jR25zgjPZ4/JbGBJ2SQI9jm3LCNK9XOrPUmZp/vu9wsvyxtmKQidpQ5+FX5w== + dependencies: + "@aws-crypto/crc32" "3.0.0" + "@aws-crypto/crc32c" "3.0.0" + "@aws-sdk/is-array-buffer" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-host-header@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.292.0.tgz#513b011fcabedf29e0a6706a4aa3867bc7d813e4" + integrity sha512-mHuCWe3Yg2S5YZ7mB7sKU6C97XspfqrimWjMW9pfV2usAvLA3R0HrB03jpR5vpZ3P4q7HB6wK3S6CjYMGGRNag== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-location-constraint@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.292.0.tgz#3b138685854cfbf48abd70c40cd56a09a74cab9a" + integrity sha512-WTbMyoCckdkmq7Yok0gI4226gTmxP/zM1fbFiC+liZXBJ+H5EvIFmu30tWbX+4m41LL/XQVm65olXJFwhoExGQ== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-logger@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.292.0.tgz#dd5ca0f20b06b1b74f918ddf0264ece1e9887aa1" + integrity sha512-yZNY1XYmG3NG+uonET7jzKXNiwu61xm/ZZ6i/l51SusuaYN+qQtTAhOFsieQqTehF9kP4FzbsWgPDwD8ZZX9lw== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-recursion-detection@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.292.0.tgz#d422bbc9efa2df2481ad56d0db553b0c0652e615" + integrity sha512-kA3VZpPko0Zqd7CYPTKAxhjEv0HJqFu2054L04dde1JLr43ro+2MTdX7vsHzeAFUVRphqatFFofCumvXmU6Mig== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-retry@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.292.0.tgz#7511f06c6e5b1b65d572ce2f596728209a2159cd" + integrity sha512-wUuXwiwMwFNMTgc9oFeUHkgpF56EfLJl/EtRn2376k9sFd7JoFu3zTo3VTGROLH/88r20A01TOr9g/cFjXgCJQ== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/service-error-classification" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-middleware" "3.292.0" + "@aws-sdk/util-retry" "3.292.0" + tslib "^2.3.1" + uuid "^8.3.2" + +"@aws-sdk/middleware-sdk-s3@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.292.0.tgz#ba921fd85417d7bc16423669234c4dbb42b0982e" + integrity sha512-kEUmh3ZM34H+2bEQfpZhVotJCNYpSbq9Q4YxlWVbnjiO/VS+S9BFEM3Fcj5+EzEgI02tNNi6/qTXj3iS8tT6hA== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-arn-parser" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-sdk-sts@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.292.0.tgz#927cecb0167b84aceddc959039f368ea2a593e87" + integrity sha512-GN5ZHEqXZqDi+HkVbaXRX9HaW/vA5rikYpWKYsmxTUZ7fB7ijvEO3co3lleJv2C+iGYRtUIHC4wYNB5xgoTCxg== + dependencies: + "@aws-sdk/middleware-signing" "3.292.0" + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/signature-v4" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-serde@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.292.0.tgz#4834ee9b03c50e11349306753c27086bac4dac08" + integrity sha512-6hN9mTQwSvV8EcGvtXbS/MpK7WMCokUku5Wu7X24UwCNMVkoRHLIkYcxHcvBTwttuOU0d8hph1/lIX4dkLwkQw== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-signing@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.292.0.tgz#51868199d23d28d264a06adcec52373c8da88c85" + integrity sha512-GVfoSjDjEQ4TaO6x9MffyP3uRV+2KcS5FtexLCYOM9pJcnE9tqq9FJOrZ1xl1g+YjUVKxo4x8lu3tpEtIb17qg== + dependencies: + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/signature-v4" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-middleware" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-ssec@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.292.0.tgz#c42a7b6a9b0e1a197d9fd6a9f497f10f785fcfd0" + integrity sha512-VfwrTEs9nYU6sCnt/cffhnJ2djGkMyMbBEysMZm2HEbFMloGKBd0Wtvk9y+SWPa6+DDRe2CqqX8jMzrO4JT4Eg== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-stack@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.292.0.tgz#279f4b688d91f9757cedd5311ae86ad6e3e6ac63" + integrity sha512-WdQpRkuMysrEwrkByCM1qCn2PPpFGGQ2iXqaFha5RzCdZDlxJni9cVNb6HzWUcgjLEYVTXCmOR9Wxm3CNW44Qg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/middleware-user-agent@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.292.0.tgz#439014fa5de7f2113110f28c1e8ef76db7ee0210" + integrity sha512-PvGMfPwfW1nq9fzWKIIS6USjY70FfdmiZhFL/TyoaTp8gV/Y1+Le8i6E1LegDbnbE/LS5IBuNgUzdserYcfbOQ== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/node-config-provider@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/node-config-provider/-/node-config-provider-3.292.0.tgz#52817db9e056fedb967704b156fde4b5516dacf1" + integrity sha512-S3NnC9dQ5GIbJYSDIldZb4zdpCOEua1tM7bjYL3VS5uqCEM93kIi/o/UkIUveMp/eqTS2LJa5HjNIz5Te6je0A== + dependencies: + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/shared-ini-file-loader" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/node-http-handler@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.292.0.tgz#f7a8fca359932ba56acf65eafd169db9d2cebc9d" + integrity sha512-L/E3UDSwXLXjt1XWWh0RBD55F+aZI1AEdPwdES9i1PjnZLyuxuDhEDptVibNN56+I9/4Q3SbmuVRVlOD0uzBag== + dependencies: + "@aws-sdk/abort-controller" "3.292.0" + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/querystring-builder" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/property-provider@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/property-provider/-/property-provider-3.292.0.tgz#2bdf9f6e15521350936636107a2057a19c1e55ec" + integrity sha512-dHArSvsiqhno/g55N815gXmAMrmN8DP7OeFNqJ4wJG42xsF2PFN3DAsjIuHuXMwu+7A3R1LHqIpvv0hA9KeoJQ== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/protocol-http@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.292.0.tgz#1829036bdec59698f44daadb590e3fa552494955" + integrity sha512-NLi4fq3k41aXIh1I97yX0JTy+3p6aW1NdwFwdMa674z86QNfb4SfRQRZBQe9wEnAZ/eWHVnlKIuII+U1URk/Kg== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/querystring-builder@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.292.0.tgz#a2fd9c2540a80718fb2f52c606926f8d2e08a695" + integrity sha512-XElIFJaReIm24eEvBtV2dOtZvcm3gXsGu/ftG8MLJKbKXFKpAP1q+K6En0Bs7/T88voKghKdKpKT+eZUWgTqlg== + dependencies: + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-uri-escape" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/querystring-parser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.292.0.tgz#32645c834b4dd1660176bf0b6df201d688242c66" + integrity sha512-iTYpYo7a8X9RxiPbjjewIpm6XQPx2EOcF1dWCPRII9EFlmZ4bwnX+PDI36fIo9oVs8TIKXmwNGODU9nsg7CSAw== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/service-error-classification@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.292.0.tgz#8fef4ee8e553218234eca91dd479902092b12bac" + integrity sha512-X1k3sixCeC45XSNHBe+kRBQBwPDyTFtFITb8O5Qw4dS9XWGhrUJT4CX0qE5aj8qP3F9U5nRizs9c2mBVVP0Caw== + +"@aws-sdk/shared-ini-file-loader@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.292.0.tgz#08260536116c4e0b44ebd0d0bd197ff15815090f" + integrity sha512-Av2TTYg1Jig2kbkD56ybiqZJB6vVrYjv1W5UQwY/q3nA/T2mcrgQ20ByCOt5Bv9VvY7FSgC+znj+L4a7RLGmBg== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/signature-v4-multi-region@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.292.0.tgz#01734775497474476d84733114b3eb129a48759e" + integrity sha512-MjWEIjbAr7n9vsFeLpoRzNSYFgWOROf1mLj6Db8TfRowaortUBO7PbleLV4n3SPujSnxhaVBzlmnCY2AjatH9g== + dependencies: + "@aws-sdk/protocol-http" "3.292.0" + "@aws-sdk/signature-v4" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-arn-parser" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/signature-v4@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.292.0.tgz#1fbb9ceea4c80c079b64f836af365985970f2a5f" + integrity sha512-+rw47VY5mvBecn13tDQTl1ipGWg5tE63faWgmZe68HoBL87ZiDzsd7bUKOvjfW21iMgWlwAppkaNNQayYRb2zg== + dependencies: + "@aws-sdk/is-array-buffer" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-hex-encoding" "3.292.0" + "@aws-sdk/util-middleware" "3.292.0" + "@aws-sdk/util-uri-escape" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/smithy-client@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.292.0.tgz#232b7bac2115d52390057bab6a79d14cffe06698" + integrity sha512-S8PKzjPkZ6SXYZuZiU787dMsvQ0d/LFEhw2OI4Oe2An9Fc2IwJ2FYukyHoQJOV2tV0DiuMebPo7eMyQyjKElvA== + dependencies: + "@aws-sdk/middleware-stack" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/token-providers@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.292.0.tgz#77fd49bbd04ac52ea27e41091478491de04a60f9" + integrity sha512-RJ+fQp/SsMnuH+WrTWaLR2Kq1b/fQdSq4zDwtauultSEBQknd7RAgjQ4JBVaIwR66vJjQPa3MXYfgja/oONT+w== + dependencies: + "@aws-sdk/client-sso-oidc" "3.292.0" + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/shared-ini-file-loader" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/types@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.292.0.tgz#54aa7347123116ac368f08df5e02954207328c63" + integrity sha512-1teYAY2M73UXZxMAxqZxVS2qwXjQh0OWtt7qyLfha0TtIk/fZ1hRwFgxbDCHUFcdNBSOSbKH/ESor90KROXLCQ== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/types@^3.222.0": + version "3.257.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.257.0.tgz#4951ee3456cd9a46829516f5596c2b8a05ffe06a" + integrity sha512-LmqXuBQBGeaGi/3Rp7XiEX1B5IPO2UUfBVvu0wwGqVsmstT0SbOVDZGPmxygACbm64n+PRx3uTSDefRfoiWYZg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/url-parser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.292.0.tgz#b8b81d1c099e248813afbc33206e24b97f14228a" + integrity sha512-NZeAuZCk1x6TIiWuRfbOU6wHPBhf0ly2qOHzWut4BCH+b4RrDmFF8EmXcH1auEfGhE7yRyR6XqIN0t3S+hYACA== + dependencies: + "@aws-sdk/querystring-parser" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-arn-parser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.292.0.tgz#079e866585ebb19aeb50fe01712e384ef90e80b0" + integrity sha512-xfE4U94TfjMC2WNNDte/kDByf16GrQKaS0BKsm+Fk/PaeHUofEp8suOEz/EVdEoa3Ayy2Uc5QdhrGnlqf8MxeA== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-base64@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64/-/util-base64-3.292.0.tgz#b07fc9752edad18b32ad4b1cc752b5df2d133377" + integrity sha512-zjNCwNdy617yFvEjZorepNWXB2sQCVfsShCwFy/kIQ5iW5tT2jQKaqc0K77diU9atkooxw9p1W9m9sOgrkOFNw== + dependencies: + "@aws-sdk/util-buffer-from" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-body-length-browser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.292.0.tgz#1baefd126c8881ff140c83111aeb79c6d5b21cb3" + integrity sha512-Wd/BM+JsMiKvKs/bN3z6TredVEHh2pKudGfg3CSjTRpqFpOG903KDfyHBD42yg5PuCHoHoewJvTPKwgn7/vhaw== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-body-length-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-node/-/util-body-length-node-3.292.0.tgz#9f3f91c80e9b4e2afb226550e9a0b3acde8bcd02" + integrity sha512-BBgipZ2P6RhogWE/qj0oqpdlyd3iSBYmb+aD/TBXwB2lA/X8A99GxweBd/kp06AmcJRoMS9WIXgbWkiiBlRlSA== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-buffer-from@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-buffer-from/-/util-buffer-from-3.292.0.tgz#b2d0eff4e63b0cc8a5d5dc133b76c3fe3daee2fc" + integrity sha512-RxNZjLoXNxHconH9TYsk5RaEBjSgTtozHeyIdacaHPj5vlQKi4hgL2hIfKeeNiAfQEVjaUFF29lv81xpNMzVMQ== + dependencies: + "@aws-sdk/is-array-buffer" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-config-provider@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-config-provider/-/util-config-provider-3.292.0.tgz#6a9c7b7e29028135862ba880c615e2f975d68c6d" + integrity sha512-t3noYll6bPRSxeeNNEkC5czVjAiTPcsq00OwfJ2xyUqmquhLEfLwoJKmrT1uP7DjIEXdUtfoIQ2jWiIVm/oO5A== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-defaults-mode-browser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.292.0.tgz#8890ee4ff8939c9ada363cae14ec7196269ff14c" + integrity sha512-7+zVUlMGfa8/KT++9humHo6IDxTnxMCmWUj5jVNlkpk6h7Ecmppf7aXotviyVIA43lhtz0p2AErs0N0ekEUK+w== + dependencies: + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/types" "3.292.0" + bowser "^2.11.0" + tslib "^2.3.1" + +"@aws-sdk/util-defaults-mode-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.292.0.tgz#fc7f54cd935b8974d1b16d6c8bed8b9ae99af20e" + integrity sha512-SSIw85eF4BVs0fOJRyshT+R3b/UmBPhiVKCUZm2rq6+lIGkDPiSwQU3d/80AhXtiL5SFT/IzAKKgQd8qMa7q3A== + dependencies: + "@aws-sdk/config-resolver" "3.292.0" + "@aws-sdk/credential-provider-imds" "3.292.0" + "@aws-sdk/node-config-provider" "3.292.0" + "@aws-sdk/property-provider" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-endpoints@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.292.0.tgz#cb6d8259efc4b3f73da0b326ef38495d9bbbf04f" + integrity sha512-CvNES1YaickVE8Iu2EP4ywdiCNy8thRnyXdx7v1d39NLeTQuMWJyM/cazWQIBv0WPYOrAnjsWb5Nw05GwpwSdA== + dependencies: + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-hex-encoding@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.292.0.tgz#a8b8b989fcf518a18606cb6d81f90d92b0660db4" + integrity sha512-qBd5KFIUywQ3qSSbj814S2srk0vfv8A6QMI+Obs1y2LHZFdQN5zViptI4UhXhKOHe+NnrHWxSuLC/LMH6q3SmA== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-locate-window@^3.0.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.55.0.tgz#a4136a20ee1bfcb73967a6614caf769ef79db070" + integrity sha512-0sPmK2JaJE2BbTcnvybzob/VrFKCXKfN4CUKcvn0yGg/me7Bz+vtzQRB3Xp+YSx+7OtWxzv63wsvHoAnXvgxgg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-middleware@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-middleware/-/util-middleware-3.292.0.tgz#d4819246c66229df405850004d9e3ae4a6fca8ea" + integrity sha512-KjhS7flfoBKDxbiBZjLjMvEizXgjfQb7GQEItgzGoI9rfGCmZtvqCcqQQoIlxb8bIzGRggAUHtBGWnlLbpb+GQ== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-retry@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-retry/-/util-retry-3.292.0.tgz#a72dd74760864aa03feb00f2cee8b97c25c297c4" + integrity sha512-JEHyF7MpVeRF5uR4LDYgpOKcFpOPiAj8TqN46SVOQQcL1K+V7cSr7O7N7J6MwJaN9XOzAcBadeIupMm7/BFbgw== + dependencies: + "@aws-sdk/service-error-classification" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-stream-browser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-stream-browser/-/util-stream-browser-3.292.0.tgz#6476fef12bea839ef25b80c79817f3932ec019d0" + integrity sha512-yzwpjq18oefyp/Sv+Z0VWh7ziRPp+qM0pDUrTfuAnXg+mrlxaPDXJOhp5LoY8AVHcDPOEdIbzz0b00G48FabIg== + dependencies: + "@aws-sdk/fetch-http-handler" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-base64" "3.292.0" + "@aws-sdk/util-hex-encoding" "3.292.0" + "@aws-sdk/util-utf8" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-stream-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-stream-node/-/util-stream-node-3.292.0.tgz#4a83004729cdc4a8fb03c96921f59939ffc31d3b" + integrity sha512-p3DHXvWo4Zdka75HwewUnWjpFp/gOT4SYYEOAsv3BwuZGxfmnojK9OVCkUBJ7s6LeHMKTgGqQPwAnVFu7iIZNg== + dependencies: + "@aws-sdk/node-http-handler" "3.292.0" + "@aws-sdk/types" "3.292.0" + "@aws-sdk/util-buffer-from" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-uri-escape@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-uri-escape/-/util-uri-escape-3.292.0.tgz#306a36e3574af3509c542c7224669082f6abc633" + integrity sha512-hOQtUMQ4VcQ9iwKz50AoCp1XBD5gJ9nly/gJZccAM7zSA5mOO8RRKkbdonqquVHxrO0CnYgiFeCh3V35GFecUw== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-user-agent-browser@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.292.0.tgz#26c4e5ffbe046cebe9d15c357839ea38ada95c56" + integrity sha512-dld+lpC3QdmTQHdBWJ0WFDkXDSrJgfz03q6mQ8+7H+BC12ZhT0I0g9iuvUjolqy7QR00OxOy47Y9FVhq8EC0Gg== + dependencies: + "@aws-sdk/types" "3.292.0" + bowser "^2.11.0" + tslib "^2.3.1" + +"@aws-sdk/util-user-agent-node@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.292.0.tgz#9065307641eb246f32fee78eec5d961cffbba6a9" + integrity sha512-f+NfIMal5E61MDc5WGhUEoicr7b1eNNhA+GgVdSB/Hg5fYhEZvFK9RZizH5rrtsLjjgcr9nPYSR7/nDKCJLumw== + dependencies: + "@aws-sdk/node-config-provider" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-utf8-browser@^3.0.0": + version "3.109.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.109.0.tgz#d013272e1981b23a4c84ac06f154db686c0cf84e" + integrity sha512-FmcGSz0v7Bqpl1SE8G1Gc0CtDpug+rvqNCG/szn86JApD/f5x8oByjbEiAyTU2ZH2VevUntx6EW68ulHyH+x+w== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-utf8@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8/-/util-utf8-3.292.0.tgz#c12049a01de36f1133232f95cbb0c0177e8d3c36" + integrity sha512-FPkj+Z59/DQWvoVu2wFaRncc3KVwe/pgK3MfVb0Lx+Ibey5KUx+sNpJmYcVYHUAe/Nv/JeIpOtYuC96IXOnI6w== + dependencies: + "@aws-sdk/util-buffer-from" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/util-waiter@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-waiter/-/util-waiter-3.292.0.tgz#860b6615f1d5d0cd545b2d5fefd0bb3c03b0a32d" + integrity sha512-+7j+mcWUY4GwU8nTK4MvLWpOzS34SJZL85qLxQ04pysoCSHkInyS51D1ejBVNlJdbUSFvIcU0WHU0y6MDDeJzg== + dependencies: + "@aws-sdk/abort-controller" "3.292.0" + "@aws-sdk/types" "3.292.0" + tslib "^2.3.1" + +"@aws-sdk/xml-builder@3.292.0": + version "3.292.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.292.0.tgz#1c53b6b4eac1b841bd5a4581289791c46cd39743" + integrity sha512-0zgnhdwUy30q/1NPXi5ekdzHQqCs3ZJaUeGbvYMO54osi4K5hygAyTsyWtv6oaJggRqZrB0LAZ9xN6hG+sA8/g== + dependencies: + tslib "^2.3.1" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/compat-data@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.6.tgz#8b37d24e88e8e21c499d4328db80577d8882fa53" + integrity sha512-tzulrgDT0QD6U7BJ4TKVk2SDDg7wlP39P9yAx1RfLy7vP/7rsDRlWVfbWxElslu56+r7QOhB2NSDsabYYruoZQ== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.6.tgz#54a107a3c298aee3fe5e1947a6464b9b6faca03d" + integrity sha512-cQbWBpxcbbs/IUredIPkHiAGULLV8iwgNRMFzvbhEXISp4f3rUUXE5+TIw6KwUWUR3DwyI6gmBRnmAtYaWehwQ== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.6" + "@babel/helper-compilation-targets" "^7.18.6" + "@babel/helper-module-transforms" "^7.18.6" + "@babel/helpers" "^7.18.6" + "@babel/parser" "^7.18.6" + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.6" + "@babel/types" "^7.18.6" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/generator@^7.18.6", "@babel/generator@^7.7.2": + version "7.18.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.7.tgz#2aa78da3c05aadfc82dbac16c99552fc802284bd" + integrity sha512-shck+7VLlY72a2w9c3zYWuE1pwOKEiQHV7GTUbSnhyl5eu3i04t30tBY82ZRWrDfo3gkakCFtevExnxbkf2a3A== + dependencies: + "@babel/types" "^7.18.7" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.6.tgz#18d35bfb9f83b1293c22c55b3d576c1315b6ed96" + integrity sha512-vFjbfhNCzqdeAtZflUFrG5YIFqGTqsctrtkZ1D/NB0mDW9TwW3GmmUepYY4G9wCET5rY5ugz4OGTcLd614IzQg== + dependencies: + "@babel/compat-data" "^7.18.6" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.20.2" + semver "^6.3.0" + +"@babel/helper-environment-visitor@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.6.tgz#b7eee2b5b9d70602e59d1a6cad7dd24de7ca6cd7" + integrity sha512-8n6gSfn2baOY+qlp+VSzsosjCVGFqWKmDF0cCWOybh52Dw3SEyoWR1KrhMJASjLwIEkkAufZ0xvr+SxLHSpy2Q== + +"@babel/helper-function-name@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.6.tgz#8334fecb0afba66e6d87a7e8c6bb7fed79926b83" + integrity sha512-0mWMxV1aC97dhjCah5U5Ua7668r5ZmSC2DLfH2EZnf9c3/dHZKiFa5pRLMH5tjSl471tY6496ZWk/kjNONBxhw== + dependencies: + "@babel/template" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-imports@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-transforms@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.6.tgz#57e3ca669e273d55c3cda55e6ebf552f37f483c8" + integrity sha512-L//phhB4al5uucwzlimruukHB3jRd5JGClwRMD/ROrVjXfLqovYnvQrK/JK36WYyVwGGO7OD3kMyVTjx+WVPhw== + dependencies: + "@babel/helper-environment-visitor" "^7.18.6" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.18.6" + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.8.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.6.tgz#9448974dd4fb1d80fefe72e8a0af37809cd30d6d" + integrity sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg== + +"@babel/helper-simple-access@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea" + integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-split-export-declaration@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-validator-identifier@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" + integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== + +"@babel/helper-validator-option@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" + integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== + +"@babel/helpers@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.6.tgz#4c966140eaa1fcaa3d5a8c09d7db61077d4debfd" + integrity sha512-vzSiiqbQOghPngUYt/zWGvK3LAsPhz55vc9XNN0xAl2gV4ieShI2OQli5duxWHD+72PZPTKAcfcZDE1Cwc5zsQ== + dependencies: + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.6.tgz#845338edecad65ebffef058d3be851f1d28a63bc" + integrity sha512-uQVSa9jJUe/G/304lXspfWVpKpK4euFLgGiMQFOCpM/bgcAdeoHwi/OQz23O9GK2osz26ZiXRRV9aV+Yl1O8tw== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.7.2": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" + integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz#1c09cd25795c7c2b8a4ba9ae49394576d4133285" + integrity sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/template@^7.18.6", "@babel/template@^7.3.3": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31" + integrity sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.18.6" + "@babel/types" "^7.18.6" + +"@babel/traverse@^7.18.6", "@babel/traverse@^7.7.2": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.6.tgz#a228562d2f46e89258efa4ddd0416942e2fd671d" + integrity sha512-zS/OKyqmD7lslOtFqbscH6gMLFYOfG1YPqCKfAW5KrTeolKqvB8UelR49Fpr6y93kYkW2Ik00mT1LOGiAGvizw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.6" + "@babel/helper-function-name" "^7.18.6" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.18.6" + "@babel/types" "^7.18.6" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.18.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.7.tgz#a4a2c910c15040ea52cdd1ddb1614a65c8041726" + integrity sha512-QG3yxTcTIBoAcQmkCs+wAPYZhu7Dk9rXKacINfNbdJDNERTbLQbHGyVG8q/YGMPeCJRIhSY0+fTc5+xuh6WPSQ== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@cspell/cspell-bundled-dicts@6.30.0": + version "6.30.0" + resolved "https://registry.yarnpkg.com/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-6.30.0.tgz#22d0256f9b7939f53d2464a42eb9c6069adf93a4" + integrity sha512-qxc2JU34DPQrHBA7L+sW6PBtIxYcVoPm3BRy95L8NzFus8MR8HeP6KqzL4Wlm3huhFhxsxKIUQukqGDTZvoHAg== + dependencies: + "@cspell/dict-ada" "^4.0.1" + "@cspell/dict-aws" "^3.0.0" + "@cspell/dict-bash" "^4.1.1" + "@cspell/dict-companies" "^3.0.9" + "@cspell/dict-cpp" "^5.0.2" + "@cspell/dict-cryptocurrencies" "^3.0.1" + "@cspell/dict-csharp" "^4.0.2" + "@cspell/dict-css" "^4.0.5" + "@cspell/dict-dart" "^2.0.2" + "@cspell/dict-django" "^4.0.2" + "@cspell/dict-docker" "^1.1.6" + "@cspell/dict-dotnet" "^5.0.0" + "@cspell/dict-elixir" "^4.0.2" + "@cspell/dict-en-common-misspellings" "^1.0.2" + "@cspell/dict-en-gb" "1.1.33" + "@cspell/dict-en_us" "^4.3.1" + "@cspell/dict-filetypes" "^3.0.0" + "@cspell/dict-fonts" "^3.0.1" + "@cspell/dict-fullstack" "^3.1.4" + "@cspell/dict-gaming-terms" "^1.0.4" + "@cspell/dict-git" "^2.0.0" + "@cspell/dict-golang" "^6.0.1" + "@cspell/dict-haskell" "^4.0.1" + "@cspell/dict-html" "^4.0.3" + "@cspell/dict-html-symbol-entities" "^4.0.0" + "@cspell/dict-java" "^5.0.5" + "@cspell/dict-k8s" "^1.0.1" + "@cspell/dict-latex" "^4.0.0" + "@cspell/dict-lorem-ipsum" "^3.0.0" + "@cspell/dict-lua" "^4.0.1" + "@cspell/dict-node" "^4.0.2" + "@cspell/dict-npm" "^5.0.5" + "@cspell/dict-php" "^4.0.1" + "@cspell/dict-powershell" "^5.0.0" + "@cspell/dict-public-licenses" "^2.0.2" + "@cspell/dict-python" "^4.0.2" + "@cspell/dict-r" "^2.0.1" + "@cspell/dict-ruby" "^5.0.0" + "@cspell/dict-rust" "^4.0.1" + "@cspell/dict-scala" "^5.0.0" + "@cspell/dict-software-terms" "^3.1.5" + "@cspell/dict-sql" "^2.1.0" + "@cspell/dict-svelte" "^1.0.2" + "@cspell/dict-swift" "^2.0.1" + "@cspell/dict-typescript" "^3.1.1" + "@cspell/dict-vue" "^3.0.0" + +"@cspell/cspell-pipe@6.30.0": + version "6.30.0" + resolved "https://registry.yarnpkg.com/@cspell/cspell-pipe/-/cspell-pipe-6.30.0.tgz#9c140522d4aec24656984c64b462110c058a5965" + integrity sha512-I0kT9co5c1whwd1s2PllZ86+BWl1DKRR5fxvodorsVTOUDeyP+A3ya1llo0+izo3iTbeCL2ckJpKp//tfP9vPA== + +"@cspell/cspell-service-bus@6.30.0": + version "6.30.0" + resolved "https://registry.yarnpkg.com/@cspell/cspell-service-bus/-/cspell-service-bus-6.30.0.tgz#5a1d94aa8277e93fd52cced7afd5549716978c80" + integrity sha512-8X20NMsPY7RMk/1uTaRDXE+yNAE9SEmSSfhUV8wL+835MFl2GY6a0wtDL+d01vUADuqJN0wcsPH8cPNvsgycPw== + +"@cspell/cspell-types@6.30.0": + version "6.30.0" + resolved "https://registry.yarnpkg.com/@cspell/cspell-types/-/cspell-types-6.30.0.tgz#94f63dac2f1773143e1219dbc79371c5f7094ec1" + integrity sha512-qKIMjLpZpFNFvKO6YzgoZWjAu287/AeLb2HfHUx0A0tX4Jfd8Ew8Y3CIG3I9CCsZlsXgAH1+vKTIg3wzVzzdhg== + +"@cspell/dict-ada@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-ada/-/dict-ada-4.0.1.tgz#214c91445eab16bd3fe10da5517f95bf2c90fe5f" + integrity sha512-/E9o3nHrXOhYmQE43deKbxZcR3MIJAsa+66IzP9TXGHheKEx8b9dVMVVqydDDH8oom1H0U20NRPtu6KRVbT9xw== + +"@cspell/dict-aws@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-aws/-/dict-aws-3.0.0.tgz#7b2db82bb632c664c3d72b83267b93b9b0cafe60" + integrity sha512-O1W6nd5y3Z00AMXQMzfiYrIJ1sTd9fB1oLr+xf/UD7b3xeHeMeYE2OtcWbt9uyeHim4tk+vkSTcmYEBKJgS5bQ== + +"@cspell/dict-bash@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-bash/-/dict-bash-4.1.1.tgz#fe28016096f44d4a09fe4c5bcaf6fa40f33d98c6" + integrity sha512-8czAa/Mh96wu2xr0RXQEGMTBUGkTvYn/Pb0o+gqOO1YW+poXGQc3gx0YPqILDryP/KCERrNvkWUJz3iGbvwC2A== + +"@cspell/dict-companies@^3.0.9": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@cspell/dict-companies/-/dict-companies-3.0.9.tgz#dfc35ad35478c8bee20a8ecd9f7509c359fe334b" + integrity sha512-wSkVIJjk33Sm3LhieNv9TsSvUSeP0R/h8xx06NqbMYF43w9J8hZiMHlbB3FzaSOHRpXT5eBIJBVTeFbceZdiqg== + +"@cspell/dict-cpp@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-cpp/-/dict-cpp-5.0.2.tgz#ab6fd2b91a08c30602426ac782a4855f239cd1e7" + integrity sha512-Q0ZjfhrHHfm0Y1/7LMCq3Fne/bhiBeBogUw4TV1wX/1tg3m+5BtaW/7GiOzRk+rFsblVj3RFam59VJKMT3vSoQ== + +"@cspell/dict-cryptocurrencies@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-cryptocurrencies/-/dict-cryptocurrencies-3.0.1.tgz#de1c235d6427946b679d23aacff12fea94e6385b" + integrity sha512-Tdlr0Ahpp5yxtwM0ukC13V6+uYCI0p9fCRGMGZt36rWv8JQZHIuHfehNl7FB/Qc09NCF7p5ep0GXbL+sVTd/+w== + +"@cspell/dict-csharp@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-csharp/-/dict-csharp-4.0.2.tgz#e55659dbe594e744d86b1baf0f3397fe57b1e283" + integrity sha512-1JMofhLK+4p4KairF75D3A924m5ERMgd1GvzhwK2geuYgd2ZKuGW72gvXpIV7aGf52E3Uu1kDXxxGAiZ5uVG7g== + +"@cspell/dict-css@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@cspell/dict-css/-/dict-css-4.0.5.tgz#2233138a03c163f82b0f6fbe0cdd2aada3ca4afc" + integrity sha512-z5vw8nJSyKd6d3i5UmMNoVcAp0wxvs9OHWOmAeJKT9fO3tok02gK24VZhcJ0NJtiKdHQ2zRuzdfWl51wdAiY6A== + +"@cspell/dict-dart@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-dart/-/dict-dart-2.0.2.tgz#714285f4f8bd304c1c477779ccbbfae5949819d7" + integrity sha512-jigcODm7Z4IFZ4vParwwP3IT0fIgRq/9VoxkXfrxBMsLBGGM2QltHBj7pl+joX+c4cOHxfyZktGJK1B1wFtR4Q== + +"@cspell/dict-django@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-django/-/dict-django-4.0.2.tgz#08d21ee3ce7e323e4d7634abf6d69a96a6d4930c" + integrity sha512-L0Yw6+Yh2bE9/FAMG4gy9m752G4V8HEBjEAGeRIQ9qvxDLR9yD6dPOtgEFTjv7SWlKSrLb9wA/W3Q2GKCOusSg== + +"@cspell/dict-docker@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@cspell/dict-docker/-/dict-docker-1.1.6.tgz#f84faed121e2093e3b212d19542fd27eda751c80" + integrity sha512-zCCiRTZ6EOQpBnSOm0/3rnKW1kCcAUDUA7SxJG3SuH6iZvKi3I8FEg8+O83WQUeXg0SyPNerD9F40JLnnJjJig== + +"@cspell/dict-dotnet@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-dotnet/-/dict-dotnet-5.0.0.tgz#13690aafe14b240ad17a30225ac1ec29a5a6a510" + integrity sha512-EOwGd533v47aP5QYV8GlSSKkmM9Eq8P3G/eBzSpH3Nl2+IneDOYOBLEUraHuiCtnOkNsz0xtZHArYhAB2bHWAw== + +"@cspell/dict-elixir@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-elixir/-/dict-elixir-4.0.2.tgz#1a37e92b45d744e1b78714c64811ca3dbc600a5c" + integrity sha512-/YeHlpZ1pE9VAyxp3V0xyUPapNyC61WwFuw2RByeoMqqYaIfS3Hw+JxtimOsAKVhUvgUH58zyKl5K5Q6FqgCpw== + +"@cspell/dict-en-common-misspellings@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-en-common-misspellings/-/dict-en-common-misspellings-1.0.2.tgz#3c4ebab8e9e906d66d60f53c8f8c2e77b7f108e7" + integrity sha512-jg7ZQZpZH7+aAxNBlcAG4tGhYF6Ksy+QS5Df73Oo+XyckBjC9QS+PrRwLTeYoFIgXy5j3ICParK5r3MSSoL4gw== + +"@cspell/dict-en-gb@1.1.33": + version "1.1.33" + resolved "https://registry.yarnpkg.com/@cspell/dict-en-gb/-/dict-en-gb-1.1.33.tgz#7f1fd90fc364a5cb77111b5438fc9fcf9cc6da0e" + integrity sha512-tKSSUf9BJEV+GJQAYGw5e+ouhEe2ZXE620S7BLKe3ZmpnjlNG9JqlnaBhkIMxKnNFkLY2BP/EARzw31AZnOv4g== + +"@cspell/dict-en_us@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-en_us/-/dict-en_us-4.3.1.tgz#88ae0fb555897ed77810867b849cc3495298f362" + integrity sha512-akfx/Q+4J3rfawtGaqe1Yp+fNyCGJCKmTQT14LXxGLN7DEjGvOFzlYoS+DdD3aDwAJih79bEFGiG+Lqs0zOauA== + +"@cspell/dict-filetypes@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-filetypes/-/dict-filetypes-3.0.0.tgz#3bb1ede3e28449f0d76024a7b918a556f210973a" + integrity sha512-Fiyp0z5uWaK0d2TfR9GMUGDKmUMAsOhGD5A0kHoqnNGswL2iw0KB0mFBONEquxU65fEnQv4R+jdM2d9oucujuA== + +"@cspell/dict-fonts@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-fonts/-/dict-fonts-3.0.1.tgz#0e0b875d463a9bd65e78145c9b6649ecad017df5" + integrity sha512-o2zVFKT3KcIBo88xlWhG4yOD0XQDjP7guc7C30ZZcSN8YCwaNc1nGoxU3QRea8iKcwk3cXH0G53nrQur7g9DjQ== + +"@cspell/dict-fullstack@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@cspell/dict-fullstack/-/dict-fullstack-3.1.4.tgz#930a66a1397f463c807e54dd01b0c79ec3f7fc21" + integrity sha512-OnCIn3GgAhdhsU6xMYes7/WXnbV6R/5k/zRAu/d+WZP4Ltf48z7oFfNFjHXH6b8ZwnMhpekLAnCeIfT5dcxRqw== + +"@cspell/dict-gaming-terms@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@cspell/dict-gaming-terms/-/dict-gaming-terms-1.0.4.tgz#b67d89d014d865da6cb40de4269d4c162a00658e" + integrity sha512-hbDduNXlk4AOY0wFxcDMWBPpm34rpqJBeqaySeoUH70eKxpxm+dvjpoRLJgyu0TmymEICCQSl6lAHTHSDiWKZg== + +"@cspell/dict-git@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-git/-/dict-git-2.0.0.tgz#fa5cb298845da9c69efc01c6af07a99097718dc9" + integrity sha512-n1AxyX5Kgxij/sZFkxFJlzn3K9y/sCcgVPg/vz4WNJ4K9YeTsUmyGLA2OQI7d10GJeiuAo2AP1iZf2A8j9aj2w== + +"@cspell/dict-golang@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-golang/-/dict-golang-6.0.1.tgz#86496bac8566fa97015f62cc81e6ec96bd98500f" + integrity sha512-Z19FN6wgg2M/A+3i1O8qhrGaxUUGOW8S2ySN0g7vp4HTHeFmockEPwYx7gArfssNIruw60JorZv+iLJ6ilTeow== + +"@cspell/dict-haskell@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-haskell/-/dict-haskell-4.0.1.tgz#e9fca7c452411ff11926e23ffed2b50bb9b95e47" + integrity sha512-uRrl65mGrOmwT7NxspB4xKXFUenNC7IikmpRZW8Uzqbqcu7ZRCUfstuVH7T1rmjRgRkjcIjE4PC11luDou4wEQ== + +"@cspell/dict-html-symbol-entities@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.0.tgz#4d86ac18a4a11fdb61dfb6f5929acd768a52564f" + integrity sha512-HGRu+48ErJjoweR5IbcixxETRewrBb0uxQBd6xFGcxbEYCX8CnQFTAmKI5xNaIt2PKaZiJH3ijodGSqbKdsxhw== + +"@cspell/dict-html@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@cspell/dict-html/-/dict-html-4.0.3.tgz#155450cb57750774583fce463d01d6323ab41701" + integrity sha512-Gae8i8rrArT0UyG1I6DHDK62b7Be6QEcBSIeWOm4VIIW1CASkN9B0qFgSVnkmfvnu1Y3H7SSaaEynKjdj3cs8w== + +"@cspell/dict-java@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@cspell/dict-java/-/dict-java-5.0.5.tgz#c673f27ce7a5d96e205f42e8be540aeda0beef11" + integrity sha512-X19AoJgWIBwJBSWGFqSgHaBR/FEykBHTMjL6EqOnhIGEyE9nvuo32tsSHjXNJ230fQxQptEvRZoaldNLtKxsRg== + +"@cspell/dict-k8s@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-k8s/-/dict-k8s-1.0.1.tgz#6c0cc521dd42fee2c807368ebfef77137686f3a1" + integrity sha512-gc5y4Nm3hVdMZNBZfU2M1AsAmObZsRWjCUk01NFPfGhFBXyVne41T7E62rpnzu5330FV/6b/TnFcPgRmak9lLw== + +"@cspell/dict-latex@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-latex/-/dict-latex-4.0.0.tgz#85054903db834ea867174795d162e2a8f0e9c51e" + integrity sha512-LPY4y6D5oI7D3d+5JMJHK/wxYTQa2lJMSNxps2JtuF8hbAnBQb3igoWEjEbIbRRH1XBM0X8dQqemnjQNCiAtxQ== + +"@cspell/dict-lorem-ipsum@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-lorem-ipsum/-/dict-lorem-ipsum-3.0.0.tgz#c6347660fcab480b47bdcaec3b57e8c3abc4af68" + integrity sha512-msEV24qEpzWZs2kcEicqYlhyBpR0amfDkJOs+iffC07si9ftqtQ+yP3lf1VFLpgqw3SQh1M1vtU7RD4sPrNlcQ== + +"@cspell/dict-lua@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-lua/-/dict-lua-4.0.1.tgz#4c31975646cb2d71f1216c7aeaa0c5ab6994ea25" + integrity sha512-j0MFmeCouSoC6EdZTbvGe1sJ9V+ruwKSeF+zRkNNNload7R72Co5kX1haW2xLHGdlq0kqSy1ODRZKdVl0e+7hg== + +"@cspell/dict-node@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-node/-/dict-node-4.0.2.tgz#9e5f64d882568fdd2a2243542d1263dbbb87c53a" + integrity sha512-FEQJ4TnMcXEFslqBQkXa5HposMoCGsiBv2ux4IZuIXgadXeHKHUHk60iarWpjhzNzQLyN2GD7NoRMd12bK3Llw== + +"@cspell/dict-npm@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@cspell/dict-npm/-/dict-npm-5.0.5.tgz#fa6c1bc983e34ddc6d97094c758a4e166afd6214" + integrity sha512-eirZm4XpJNEcbmLGIwI2qXdRRlCKwEsH9mT3qCUytmbj6S6yn63F+8bShMW/yQBedV7+GXq9Td+cJdqiVutOiA== + +"@cspell/dict-php@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-php/-/dict-php-4.0.1.tgz#f3c5cd241f43a32b09355370fc6ce7bd50e6402c" + integrity sha512-XaQ/JkSyq2c07MfRG54DjLi2CV+HHwS99DDCAao9Fq2JfkWroTQsUeek7wYZXJATrJVOULoV3HKih12x905AtQ== + +"@cspell/dict-powershell@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-powershell/-/dict-powershell-5.0.0.tgz#c49fbefb6991f11d99ca77114b3892b6d9d3865d" + integrity sha512-UdMcc5kC6tNRBwl29RyMFa3UBPjnG7p5+8tMIZknRRU3TclxiYW6EJJhlBSYxK0V0PPe+KcEPHPD3ypshLQkOw== + +"@cspell/dict-public-licenses@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-public-licenses/-/dict-public-licenses-2.0.2.tgz#81f0fde2e78bf8160ce988ae6961003a3cde3d56" + integrity sha512-baKkbs/WGEV2lCWZoL0KBPh3uiPcul5GSDwmXEBAsR5McEW52LF94/b7xWM0EmSAc/y8ODc5LnPYC7RDRLi6LQ== + +"@cspell/dict-python@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-python/-/dict-python-4.0.2.tgz#36582b21c1fda7f54d95052968b845dd595b585f" + integrity sha512-w1jSWDR1CkO23cZFbSYgnD/ZqknDZSVCI1AOE6sSszOJR8shmBkV3lMBYd+vpLsWhmkLLBcZTXDkiqFLXDGowQ== + +"@cspell/dict-r@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-r/-/dict-r-2.0.1.tgz#73474fb7cce45deb9094ebf61083fbf5913f440a" + integrity sha512-KCmKaeYMLm2Ip79mlYPc8p+B2uzwBp4KMkzeLd5E6jUlCL93Y5Nvq68wV5fRLDRTf7N1LvofkVFWfDcednFOgA== + +"@cspell/dict-ruby@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-ruby/-/dict-ruby-5.0.0.tgz#ca22ddf0842f29b485e3ef585c666c6be5227e6d" + integrity sha512-ssb96QxLZ76yPqFrikWxItnCbUKhYXJ2owkoIYzUGNFl2CHSoHCb5a6Zetum9mQ/oUA3gNeUhd28ZUlXs0la2A== + +"@cspell/dict-rust@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-rust/-/dict-rust-4.0.1.tgz#ef0b88cb3a45265824e2c9ce31b0baa4e1050351" + integrity sha512-xJSSzHDK2z6lSVaOmMxl3PTOtfoffaxMo7fTcbZUF+SCJzfKbO6vnN9TCGX2sx1RHFDz66Js6goz6SAZQdOwaw== + +"@cspell/dict-scala@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-scala/-/dict-scala-5.0.0.tgz#b64365ad559110a36d44ccd90edf7151ea648022" + integrity sha512-ph0twaRoV+ylui022clEO1dZ35QbeEQaKTaV2sPOsdwIokABPIiK09oWwGK9qg7jRGQwVaRPEq0Vp+IG1GpqSQ== + +"@cspell/dict-software-terms@^3.1.5": + version "3.1.5" + resolved "https://registry.yarnpkg.com/@cspell/dict-software-terms/-/dict-software-terms-3.1.5.tgz#9000ba07df6d868df257ca2438df36a0eb74acc6" + integrity sha512-wmkWHHkp2AN9EDWNBLB0VASB5OtsC3KnhoAHxCJzC6AB3xjYoBfKsvgI/o50gfbsCVQceHpqXjOEYSw/xxTKNw== + +"@cspell/dict-sql@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-sql/-/dict-sql-2.1.0.tgz#4210e83b9fc05ef91f577ae44fd264825ccfbf71" + integrity sha512-Bb+TNWUrTNNABO0bmfcYXiTlSt0RD6sB2MIY+rNlaMyIwug43jUjeYmkLz2tPkn3+2uvySeFEOMVYhMVfcuDKg== + +"@cspell/dict-svelte@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-svelte/-/dict-svelte-1.0.2.tgz#0c866b08a7a6b33bbc1a3bdbe6a1b484ca15cdaa" + integrity sha512-rPJmnn/GsDs0btNvrRBciOhngKV98yZ9SHmg8qI6HLS8hZKvcXc0LMsf9LLuMK1TmS2+WQFAan6qeqg6bBxL2Q== + +"@cspell/dict-swift@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-swift/-/dict-swift-2.0.1.tgz#06ec86e52e9630c441d3c19605657457e33d7bb6" + integrity sha512-gxrCMUOndOk7xZFmXNtkCEeroZRnS2VbeaIPiymGRHj5H+qfTAzAKxtv7jJbVA3YYvEzWcVE2oKDP4wcbhIERw== + +"@cspell/dict-typescript@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-typescript/-/dict-typescript-3.1.1.tgz#25a9c241fa79c032f907db21b0aaf7c7baee6cc3" + integrity sha512-N9vNJZoOXmmrFPR4ir3rGvnqqwmQGgOYoL1+y6D4oIhyr7FhaYiyF/d7QT61RmjZQcATMa6PSL+ZisCeRLx9+A== + +"@cspell/dict-vue@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@cspell/dict-vue/-/dict-vue-3.0.0.tgz#68ccb432ad93fcb0fd665352d075ae9a64ea9250" + integrity sha512-niiEMPWPV9IeRBRzZ0TBZmNnkK3olkOPYxC1Ny2AX4TGlYRajcW0WUtoSHmvvjZNfWLSg2L6ruiBeuPSbjnG6A== + +"@cspell/dynamic-import@6.30.0": + version "6.30.0" + resolved "https://registry.yarnpkg.com/@cspell/dynamic-import/-/dynamic-import-6.30.0.tgz#73a716020e1bd0cd298817c32451b03ad2393f69" + integrity sha512-081r5p0cfHNhYidBtp6tNkZBJHUx2oM79BsBDf5Epx8ikqb6TLJMTZ5egsCCe2eXokaufwVyRiIfRLe4jKkHqQ== + dependencies: + import-meta-resolve "^2.2.2" + +"@cspell/strong-weak-map@6.30.0": + version "6.30.0" + resolved "https://registry.yarnpkg.com/@cspell/strong-weak-map/-/strong-weak-map-6.30.0.tgz#800c34f516fba465871a0776133000680e702ad7" + integrity sha512-IBYhq9DpVElqYcrJcPjZazqPQ896cyqTrPiszuCs58roweHm1KwB2PlzFx2nerig/dwbwImKr+4QbOC6BbqnLw== + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@dabh/diagnostics@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" + integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + +"@eslint-community/eslint-utils@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz#a831e6e468b4b2b5ae42bf658bea015bf10bc518" + integrity sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.4.0.tgz#3e61c564fcd6b921cb789838631c5ee44df09403" + integrity sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ== + +"@eslint/eslintrc@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.1.tgz#7888fe7ec8f21bc26d646dbd2c11cd776e21192d" + integrity sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.5.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.36.0": + version "8.36.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe" + integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg== + +"@faker-js/faker@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-7.6.0.tgz#9ea331766084288634a9247fcd8b84f16ff4ba07" + integrity sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw== + +"@hapi/hoek@^9.0.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@humanwhocodes/config-array@^0.11.8": + version "0.11.8" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" + integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.5.0.tgz#593a6c5c0d3f75689835f1b3b4688c4f8544cb57" + integrity sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ== + dependencies: + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.5.0" + jest-util "^29.5.0" + slash "^3.0.0" + +"@jest/core@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.5.0.tgz#76674b96904484e8214614d17261cc491e5f1f03" + integrity sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ== + dependencies: + "@jest/console" "^29.5.0" + "@jest/reporters" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.5.0" + jest-config "^29.5.0" + jest-haste-map "^29.5.0" + jest-message-util "^29.5.0" + jest-regex-util "^29.4.3" + jest-resolve "^29.5.0" + jest-resolve-dependencies "^29.5.0" + jest-runner "^29.5.0" + jest-runtime "^29.5.0" + jest-snapshot "^29.5.0" + jest-util "^29.5.0" + jest-validate "^29.5.0" + jest-watcher "^29.5.0" + micromatch "^4.0.4" + pretty-format "^29.5.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.5.0.tgz#9152d56317c1fdb1af389c46640ba74ef0bb4c65" + integrity sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ== + dependencies: + "@jest/fake-timers" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + jest-mock "^29.5.0" + +"@jest/expect-utils@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.0.1.tgz#c1a84ee66caaef537f351dd82f7c63d559cf78d5" + integrity sha512-Tw5kUUOKmXGQDmQ9TSgTraFFS7HMC1HG/B7y0AN2G2UzjdAXz9BzK2rmNpCSDl7g7y0Gf/VLBm//blonvhtOTQ== + dependencies: + jest-get-type "^29.0.0" + +"@jest/expect-utils@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.5.0.tgz#f74fad6b6e20f924582dc8ecbf2cb800fe43a036" + integrity sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg== + dependencies: + jest-get-type "^29.4.3" + +"@jest/expect@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.5.0.tgz#80952f5316b23c483fbca4363ce822af79c38fba" + integrity sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g== + dependencies: + expect "^29.5.0" + jest-snapshot "^29.5.0" + +"@jest/fake-timers@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.5.0.tgz#d4d09ec3286b3d90c60bdcd66ed28d35f1b4dc2c" + integrity sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg== + dependencies: + "@jest/types" "^29.5.0" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.5.0" + jest-mock "^29.5.0" + jest-util "^29.5.0" + +"@jest/globals@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.5.0.tgz#6166c0bfc374c58268677539d0c181f9c1833298" + integrity sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/expect" "^29.5.0" + "@jest/types" "^29.5.0" + jest-mock "^29.5.0" + +"@jest/reporters@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.5.0.tgz#985dfd91290cd78ddae4914ba7921bcbabe8ac9b" + integrity sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@jridgewell/trace-mapping" "^0.3.15" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^5.1.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.5.0" + jest-util "^29.5.0" + jest-worker "^29.5.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^29.0.0": + version "29.0.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.0.0.tgz#5f47f5994dd4ef067fb7b4188ceac45f77fe952a" + integrity sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA== + dependencies: + "@sinclair/typebox" "^0.24.1" + +"@jest/schemas@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.3.tgz#39cf1b8469afc40b6f5a2baaa146e332c4151788" + integrity sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg== + dependencies: + "@sinclair/typebox" "^0.25.16" + +"@jest/source-map@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.4.3.tgz#ff8d05cbfff875d4a791ab679b4333df47951d20" + integrity sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.15" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.5.0.tgz#7c856a6ca84f45cc36926a4e9c6b57f1973f1408" + integrity sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ== + dependencies: + "@jest/console" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz#34d7d82d3081abd523dbddc038a3ddcb9f6d3cc4" + integrity sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ== + dependencies: + "@jest/test-result" "^29.5.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.5.0" + slash "^3.0.0" + +"@jest/transform@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.5.0.tgz#cf9c872d0965f0cbd32f1458aa44a2b1988b00f9" + integrity sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.5.0" + "@jridgewell/trace-mapping" "^0.3.15" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.5.0" + jest-regex-util "^29.4.3" + jest-util "^29.5.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^29.0.1": + version "29.0.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.0.1.tgz#1985650acf137bdb81710ff39a4689ec071dd86a" + integrity sha512-ft01rxzVsbh9qZPJ6EFgAIj3PT9FCRfBF9Xljo2/33VDOUjLZr0ZJ2oKANqh9S/K0/GERCsHDAQlBwj7RxA+9g== + dependencies: + "@jest/schemas" "^29.0.0" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jest/types@^29.0.2": + version "29.0.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.0.2.tgz#5a5391fa7f7f41bf4b201d6d2da30e874f95b6c1" + integrity sha512-5WNMesBLmlkt1+fVkoCjHa0X3i3q8zc4QLTDkdHgCa2gyPZc7rdlZBWgVLqwS1860ZW5xJuCDwAzqbGaXIr/ew== + dependencies: + "@jest/schemas" "^29.0.0" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jest/types@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.5.0.tgz#f59ef9b031ced83047c67032700d8c807d6e1593" + integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog== + dependencies: + "@jest/schemas" "^29.4.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@joi/date@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@joi/date/-/date-2.1.0.tgz#fa7e3069a63c17d9ebe51a0b667f090c288de237" + integrity sha512-2zN5m0LgxZp/cynHGbzEImVmFIa+n+IOb/Nlw5LX/PLJneeCwG1NbiGw7MvPjsAKUGQK8z31Nn6V6lEN+4fZhg== + dependencies: + moment "2.x.x" + +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.14" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" + integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.15": + version "0.3.15" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" + integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@lukeed/csprng@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.0.1.tgz#625e93a0edb2c830e3c52ce2d67b9d53377c6a66" + integrity sha512-uSvJdwQU5nK+Vdf6zxcWAY2A8r7uqe+gePwLWzJ+fsQehq18pc0I2hJKwypZ2aLM90+Er9u1xn4iLJPZ+xlL4g== + +"@nestjs/axios@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-2.0.0.tgz#2116fad483e232ef102a877b503a9f19926bd102" + integrity sha512-F6oceoQLEn031uun8NiommeMkRIojQqVryxQy/mK7fx0CI0KbgkJL3SloCQcsOD+agoEnqKJKXZpEvL6FNswJg== + +"@nestjs/cli@^9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-9.2.0.tgz#d174f54d7aaa6695b8e413093e3d18367bc8bec7" + integrity sha512-6B1IjDcJbrOu55oMF67L1x5lDUOZ3Zs9l7bKCBH9D78965m8wq/2rlEWl/gJto5TABLQWy3hVvV/s8VzUlRMxw== + dependencies: + "@angular-devkit/core" "15.1.4" + "@angular-devkit/schematics" "15.1.4" + "@angular-devkit/schematics-cli" "15.1.4" + "@nestjs/schematics" "^9.0.0" + chalk "3.0.0" + chokidar "3.5.3" + cli-table3 "0.6.3" + commander "4.1.1" + fork-ts-checker-webpack-plugin "7.3.0" + inquirer "7.3.3" + node-emoji "1.11.0" + ora "5.4.1" + os-name "4.0.1" + rimraf "4.1.2" + shelljs "0.8.5" + source-map-support "0.5.21" + tree-kill "1.2.2" + tsconfig-paths "4.1.2" + tsconfig-paths-webpack-plugin "4.0.0" + typescript "4.9.5" + webpack "5.75.0" + webpack-node-externals "3.0.0" + +"@nestjs/common@^9.3.10": + version "9.3.10" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-9.3.10.tgz#c137402cad41123eaf5c74c2404e92490f9bbd50" + integrity sha512-wj2bM9TXBlAvzgznkID0s7bN/niVn90sZIDtRFDnvaB1qagEpkWA0Bt39qilIuqdReluIaCjeEW106U0oyz+mQ== + dependencies: + uid "2.0.1" + iterare "1.2.1" + tslib "2.5.0" + +"@nestjs/config@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-2.3.1.tgz#6ac151f818db4ccf987c7ff8ef5b2c1f4eeec913" + integrity sha512-Ckzel0NZ9CWhNsLfE1hxfDuxJuEbhQvGxSlmZ1/X8awjRmAA/g3kT6M1+MO1SHj1wMtPyUfd9WpwkiqFbiwQgA== + dependencies: + dotenv "16.0.3" + dotenv-expand "10.0.0" + lodash "4.17.21" + uuid "9.0.0" + +"@nestjs/core@^9.3.10": + version "9.3.10" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-9.3.10.tgz#34549387f60d3a6c2a5cc438f0f6a55592c65914" + integrity sha512-9QuE5jHtRnqKZULUhQoB0pNU26mwJ4hYNwAQ7lc/nFU2/4Ci+wTTRrXhLZeirgaF6TLCgLQB7/wLHImcfoXUog== + dependencies: + uid "2.0.1" + "@nuxtjs/opencollective" "0.3.2" + fast-safe-stringify "2.1.1" + iterare "1.2.1" + path-to-regexp "3.2.0" + tslib "2.5.0" + +"@nestjs/jwt@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@nestjs/jwt/-/jwt-10.0.2.tgz#bc049fe1622299e456a849999ebf30cb56ab4dd1" + integrity sha512-MLxjCSbO7C9fN2hst5kpIhnJAgglJmrKppXAXqElB8A9ip3ZuCowMDjjmNWWJyfOzE98NV0E0iEQGE2StMUC+Q== + dependencies: + "@types/jsonwebtoken" "9.0.1" + jsonwebtoken "9.0.0" + +"@nestjs/mapped-types@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-1.2.2.tgz#d9ddb143776e309dbc1a518ac1607fddac1e140e" + integrity sha512-3dHxLXs3M0GPiriAcCFFJQHoDFUuzTD5w6JDhE7TyfT89YKpe6tcCCIqOZWdXmt9AZjjK30RkHRSFF+QEnWFQg== + +"@nestjs/mongoose@^9.2.1": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@nestjs/mongoose/-/mongoose-9.2.1.tgz#b880feb7ce52a081e81374ddf3c3ed0eff1224f4" + integrity sha512-tMK5kKFjQnNVhqJDw1wa352z+VsODOFznTn74xSzrziof03qS+O6rLU4q1kMx0B4AmFbADf03GOdpvBc9bMWqw== + +"@nestjs/passport@^9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@nestjs/passport/-/passport-9.0.3.tgz#4df0e6de3176e04a5770cb432e58f129c8e49f9e" + integrity sha512-HplSJaimEAz1IOZEu+pdJHHJhQyBOPAYWXYHfAPQvRqWtw4FJF1VXl1Qtk9dcXQX1eKytDtH+qBzNQc19GWNEg== + +"@nestjs/platform-express@^9.3.10": + version "9.3.10" + resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-9.3.10.tgz#5493bd4dc3f5f28e3224afd56d113017b746f3d6" + integrity sha512-5aWokr8s0pipD5c/n40xC1iv3cMXfWrOhciX430p53cy4uyTAE+sTBk0PhB6tdG8NpK33aNqqHz/tyKlauQu/Q== + dependencies: + body-parser "1.20.2" + cors "2.8.5" + express "4.18.2" + multer "1.4.4-lts.1" + tslib "2.5.0" + +"@nestjs/schedule@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@nestjs/schedule/-/schedule-2.2.0.tgz#6e6e55648d9fa03dfc861354d00da2985161753b" + integrity sha512-wrDnUONTxBkD6lTWh9ecYk/kvJTbA3PylotjBoRsECmcS1SNvgInFXuL38UnHiFnXM3CHSFnzRLB259Bc1mOdQ== + dependencies: + cron "2.2.0" + uuid "9.0.0" + +"@nestjs/schematics@^9.0.0": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-9.0.1.tgz#2ec1b3fc4cd2c44310d4d7d1f5f276a18d24964b" + integrity sha512-QU7GbnQvADFXdumcdADmv4vil3bhnYl2IFHWKieRt0MgIhghgBxIB7kDKWhswcuZ0kZztVbyYjo9aCrlf62fcw== + dependencies: + "@angular-devkit/core" "14.0.5" + "@angular-devkit/schematics" "14.0.5" + fs-extra "10.1.0" + jsonc-parser "3.0.0" + pluralize "8.0.0" + +"@nestjs/schematics@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-9.0.4.tgz#ab612f5a8e006ca1d617eddc8143ee00b766312b" + integrity sha512-egurCfAc4e5i1r2TmeAF0UrOKejFmT5oTdv4b7HcOVPupc3QGU7CbEfGleL3mkM5AjrixTQeMxU9bJ00ttAbGg== + dependencies: + "@angular-devkit/core" "15.0.4" + "@angular-devkit/schematics" "15.0.4" + fs-extra "11.1.0" + jsonc-parser "3.2.0" + pluralize "8.0.0" + +"@nestjs/swagger@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-6.2.1.tgz#38caa76ac00993ddc11a79dd24ab9f4392d2791d" + integrity sha512-9M2vkfJHIzLqDZwvM5TEZO0MxRCvIb0xVy0LsmWwxH1lrb0z/4MhU+r2CWDhBtTccVJrKxVPiU2s3T3b9uUJbg== + dependencies: + "@nestjs/mapped-types" "1.2.2" + js-yaml "4.1.0" + lodash "4.17.21" + path-to-regexp "3.2.0" + swagger-ui-dist "4.15.5" + +"@nestjs/terminus@^9.2.1": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@nestjs/terminus/-/terminus-9.2.1.tgz#f173ba807bbab6ed2ee892e859455553274a725e" + integrity sha512-bPJsxKzqLl1BIs1YFIji20h42VG4ElGqc+lyw7nW+as0DkfjpRYUdyEBQJo6dTAcqRrVxSN2m3wKweBknK3Nxw== + dependencies: + boxen "5.1.2" + check-disk-space "3.3.1" + +"@nestjs/testing@^9.3.10": + version "9.3.10" + resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-9.3.10.tgz#d0229d4d338806758dae824bdbeb935e097d4901" + integrity sha512-TGspJkzDx1YmJzlmNG5WrhFa7IGgXbCVt4UXvBVqEk2QRPmJFZnqd0T9waKZ+SxwH4gY5sdw2niTFvOgqGVfJw== + dependencies: + tslib "2.5.0" + +"@nestjs/throttler@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@nestjs/throttler/-/throttler-4.0.0.tgz#9f9c66df62da6ec1b1ad4305e6a3865676e3812f" + integrity sha512-2T2S/hFhxROa/PZRZhHWFkrukxg3T8Db32Y04m6U6j6N2XqFGSKXhjfIbORO8kk/S2jswa9oTX/K12E120tgaQ== + dependencies: + md5 "^2.2.1" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@nuxtjs/opencollective@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz#620ce1044f7ac77185e825e1936115bb38e2681c" + integrity sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA== + dependencies: + chalk "^4.1.0" + consola "^2.15.0" + node-fetch "^2.6.1" + +"@sideway/address@^4.1.3": + version "4.1.4" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" + integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + +"@sinclair/typebox@^0.24.1": + version "0.24.19" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.19.tgz#5297278e0d8a1aea084685a3216074910ac6c113" + integrity sha512-gHJu8cdYTD5p4UqmQHrxaWrtb/jkH5imLXzuBypWhKzNkW0qfmgz+w1xaJccWVuJta1YYUdlDiPHXRTR4Ku0MQ== + +"@sinclair/typebox@^0.25.16": + version "0.25.21" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.21.tgz#763b05a4b472c93a8db29b2c3e359d55b29ce272" + integrity sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g== + +"@sinonjs/commons@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" + integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c" + integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw== + dependencies: + "@sinonjs/commons" "^2.0.0" + +"@ts-morph/common@~0.12.3": + version "0.12.3" + resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.12.3.tgz#a96e250217cd30e480ab22ec6a0ebbe65fd784ff" + integrity sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w== + dependencies: + fast-glob "^3.2.7" + minimatch "^3.0.4" + mkdirp "^1.0.4" + path-browserify "^1.0.1" + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + +"@types/babel__core@^7.1.14": + version "7.1.19" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" + integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.17.1" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.17.1.tgz#1a0e73e8c28c7e832656db372b779bfd2ef37314" + integrity sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA== + dependencies: + "@babel/types" "^7.3.0" + +"@types/bcryptjs@^2.4.2": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.2.tgz#e3530eac9dd136bfdfb0e43df2c4c5ce1f77dfae" + integrity sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ== + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bytes@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/bytes/-/bytes-3.1.1.tgz#67a876422e660dc4c10a27f3e5bcfbd5455f01d0" + integrity sha512-lOGyCnw+2JVPKU3wIV0srU0NyALwTBJlVSx5DfMQOFuuohA8y9S8orImpuIQikZ0uIQ8gehrRjxgQC1rLRi11w== + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/cookiejar@*": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" + integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== + +"@types/cors@^2.8.13": + version "2.8.13" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.13.tgz#b8ade22ba455a1b8cb3b5d3f35910fd204f84f94" + integrity sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA== + dependencies: + "@types/node" "*" + +"@types/cron@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/cron/-/cron-2.0.0.tgz#4fe75f2720a3b69a1f7b80e656749f4c2c96d727" + integrity sha512-xZM08fqvwIXgghtPVkSPKNgC+JoMQ2OHazEvyTKnNf7aWu1aB6/4lBbQFrb03Td2cUGG7ITzMv3mFYnMu6xRaQ== + dependencies: + "@types/luxon" "*" + "@types/node" "*" + +"@types/crypto-js@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d" + integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA== + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.4.5" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.5.tgz#acdfb7dd36b91cc5d812d7c093811a8f3d9b31e4" + integrity sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*": + version "0.0.52" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.52.tgz#7f1f57ad5b741f3d5b210d3b1f145640d89bf8fe" + integrity sha512-BZWrtCU0bMVAIliIV+HJO1f1PR41M7NKjfxrFJwwhKI1KwhwOxYw1SXg9ao+CIMt774nFuGiG6eU+udtbEI9oQ== + +"@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + +"@types/express-serve-static-core@^4.17.18": + version "4.17.29" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz#2a1795ea8e9e9c91b4a4bbe475034b20c1ec711c" + integrity sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.17.33" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543" + integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" + integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.18" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/express@^4.17.17": + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" + integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/graceful-fs@^4.1.3": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" + integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@^29.4.4": + version "29.4.4" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.4.4.tgz#ba257bd7b1876dec9e0b4fb82fa60eec5505e5f1" + integrity sha512-qezb65VIH7X1wobSnd6Lvdve7PXSyQRa3dljTkhTtDhi603RvHQCshSlJcuyMLHJpeHgY3NKwvDJWxMOOHxGDQ== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + +"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/jsonwebtoken@*": + version "8.5.8" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz#01b39711eb844777b7af1d1f2b4cf22fda1c0c44" + integrity sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A== + dependencies: + "@types/node" "*" + +"@types/jsonwebtoken@9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#29b1369c4774200d6d6f63135bf3d1ba3ef997a4" + integrity sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw== + dependencies: + "@types/node" "*" + +"@types/lodash@^4.14.191": + version "4.14.191" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" + integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== + +"@types/luxon@*": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-2.3.2.tgz#8a3f2cdd4858ce698b56cd8597d9243b8e9d3c65" + integrity sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA== + +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + +"@types/morgan@^1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.9.4.tgz#99965ad2bdc7c5cee28d8ce95cfa7300b19ea562" + integrity sha512-cXoc4k+6+YAllH3ZHmx4hf7La1dzUk6keTR4bF4b4Sc0mZxU/zK4wO7l+ZzezXm/jkYj/qC+uYGZrarZdIVvyQ== + dependencies: + "@types/node" "*" + +"@types/ms@^0.7.31": + version "0.7.31" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + +"@types/multer@^1.4.7": + version "1.4.7" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.7.tgz#89cf03547c28c7bbcc726f029e2a76a7232cc79e" + integrity sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA== + dependencies: + "@types/express" "*" + +"@types/node@*": + version "18.0.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.3.tgz#463fc47f13ec0688a33aec75d078a0541a447199" + integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ== + +"@types/node@^18.15.3": + version "18.15.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014" + integrity sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/passport-jwt@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/passport-jwt/-/passport-jwt-3.0.8.tgz#c8a95bf7d8f330f2560f1b3d07605e23ac01469a" + integrity sha512-VKJZDJUAHFhPHHYvxdqFcc5vlDht8Q2pL1/ePvKAgqRThDaCc84lSYOTQmnx3+JIkDlN+2KfhFhXIzlcVT+Pcw== + dependencies: + "@types/express" "*" + "@types/jsonwebtoken" "*" + "@types/passport-strategy" "*" + +"@types/passport-strategy@*": + version "0.2.35" + resolved "https://registry.yarnpkg.com/@types/passport-strategy/-/passport-strategy-0.2.35.tgz#e52f5212279ea73f02d9b06af67efe9cefce2d0c" + integrity sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g== + dependencies: + "@types/express" "*" + "@types/passport" "*" + +"@types/passport@*": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.9.tgz#b32fa8f7485dace77a9b58e82d0c92908f6e8387" + integrity sha512-9+ilzUhmZQR4JP49GdC2O4UdDE3POPLwpmaTC/iLkW7l0TZCXOo1zsTnnlXPq6rP1UsUZPfbAV4IUdiwiXyC7g== + dependencies: + "@types/express" "*" + +"@types/prettier@^2.1.5": + version "2.6.3" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.6.3.tgz#68ada76827b0010d0db071f739314fa429943d0a" + integrity sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/response-time@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@types/response-time/-/response-time-2.3.5.tgz#e85ff348caefd0f8d3e8902424c681a59aafc31e" + integrity sha512-4ANzp+I3K7sztFFAGPALWBvSl4ayaDSKzI2Bok+WNz+en2eB2Pvk6VCjR47PBXBWOkEg2r4uWpZOlXA5DNINOQ== + dependencies: + "@types/express" "*" + "@types/node" "*" + +"@types/semver@^7.3.12": + version "7.3.12" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.12.tgz#920447fdd78d76b19de0438b7f60df3c4a80bf1c" + integrity sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A== + +"@types/serve-static@*": + version "1.13.10" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" + integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + +"@types/superagent@*": + version "4.1.15" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.15.tgz#63297de457eba5e2bc502a7609426c4cceab434a" + integrity sha512-mu/N4uvfDN2zVQQ5AYJI/g4qxn2bHB6521t1UuH09ShNWjebTqN0ZFuYK9uYjcgmI0dTQEs+Owi1EO6U0OkOZQ== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.12": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" + integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== + dependencies: + "@types/superagent" "*" + +"@types/ua-parser-js@^0.7.36": + version "0.7.36" + resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190" + integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ== + +"@types/uuid@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.1.tgz#98586dc36aee8dacc98cc396dbca8d0429647aa6" + integrity sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA== + +"@types/validator@^13.7.10": + version "13.7.10" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.10.tgz#f9763dc0933f8324920afa9c0790308eedf55ca7" + integrity sha512-t1yxFAR2n0+VO6hd/FJ9F2uezAZVWHLmpmlJzm1eX03+H7+HsuTAp7L8QJs+2pQCfWkP1+EXsGK9Z9v7o/qPVQ== + +"@types/webidl-conversions@*": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz#e33bc8ea812a01f63f90481c666334844b12a09e" + integrity sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q== + +"@types/whatwg-url@^8.2.1": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-8.2.2.tgz#749d5b3873e845897ada99be4448041d4cc39e63" + integrity sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA== + dependencies: + "@types/node" "*" + "@types/webidl-conversions" "*" + +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^17.0.8": + version "17.0.10" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.10.tgz#591522fce85d8739bca7b8bb90d048e4478d186a" + integrity sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.55.0.tgz#bc2400c3a23305e8c9a9c04aa40933868aaaeb47" + integrity sha512-IZGc50rtbjk+xp5YQoJvmMPmJEYoC53SiKPXyqWfv15XoD2Y5Kju6zN0DwlmaGJp1Iw33JsWJcQ7nw0lGCGjVg== + dependencies: + "@eslint-community/regexpp" "^4.4.0" + "@typescript-eslint/scope-manager" "5.55.0" + "@typescript-eslint/type-utils" "5.55.0" + "@typescript-eslint/utils" "5.55.0" + debug "^4.3.4" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/parser@^5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.55.0.tgz#8c96a0b6529708ace1dcfa60f5e6aec0f5ed2262" + integrity sha512-ppvmeF7hvdhUUZWSd2EEWfzcFkjJzgNQzVST22nzg958CR+sphy8A6K7LXQZd6V75m1VKjp+J4g/PCEfSCmzhw== + dependencies: + "@typescript-eslint/scope-manager" "5.55.0" + "@typescript-eslint/types" "5.55.0" + "@typescript-eslint/typescript-estree" "5.55.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.55.0.tgz#e863bab4d4183ddce79967fe10ceb6c829791210" + integrity sha512-OK+cIO1ZGhJYNCL//a3ROpsd83psf4dUJ4j7pdNVzd5DmIk+ffkuUIX2vcZQbEW/IR41DYsfJTB19tpCboxQuw== + dependencies: + "@typescript-eslint/types" "5.55.0" + "@typescript-eslint/visitor-keys" "5.55.0" + +"@typescript-eslint/type-utils@5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.55.0.tgz#74bf0233523f874738677bb73cb58094210e01e9" + integrity sha512-ObqxBgHIXj8rBNm0yh8oORFrICcJuZPZTqtAFh0oZQyr5DnAHZWfyw54RwpEEH+fD8suZaI0YxvWu5tYE/WswA== + dependencies: + "@typescript-eslint/typescript-estree" "5.55.0" + "@typescript-eslint/utils" "5.55.0" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.55.0.tgz#9830f8d3bcbecf59d12f821e5bc6960baaed41fd" + integrity sha512-M4iRh4AG1ChrOL6Y+mETEKGeDnT7Sparn6fhZ5LtVJF1909D5O4uqK+C5NPbLmpfZ0XIIxCdwzKiijpZUOvOug== + +"@typescript-eslint/typescript-estree@5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.55.0.tgz#8db7c8e47ecc03d49b05362b8db6f1345ee7b575" + integrity sha512-I7X4A9ovA8gdpWMpr7b1BN9eEbvlEtWhQvpxp/yogt48fy9Lj3iE3ild/1H3jKBBIYj5YYJmS2+9ystVhC7eaQ== + dependencies: + "@typescript-eslint/types" "5.55.0" + "@typescript-eslint/visitor-keys" "5.55.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.55.0.tgz#34e97322e7ae5b901e7a870aabb01dad90023341" + integrity sha512-FkW+i2pQKcpDC3AY6DU54yl8Lfl14FVGYDgBTyGKB75cCwV3KpkpTMFi9d9j2WAJ4271LR2HeC5SEWF/CZmmfw== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.55.0" + "@typescript-eslint/types" "5.55.0" + "@typescript-eslint/typescript-estree" "5.55.0" + eslint-scope "^5.1.1" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.55.0": + version "5.55.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.55.0.tgz#01ad414fca8367706d76cdb94adf788dc5b664a2" + integrity sha512-q2dlHHwWgirKh1D3acnuApXG+VNXpEY5/AwRxDVuEQpxWaB0jCDe0jFMVMALJ3ebSfuOVE8/rMS+9ZOYGg1GWw== + dependencies: + "@typescript-eslint/types" "5.55.0" + eslint-visitor-keys "^3.3.0" + +"@webassemblyjs/ast@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" + integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + +"@webassemblyjs/floating-point-hex-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" + integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== + +"@webassemblyjs/helper-api-error@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" + integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== + +"@webassemblyjs/helper-buffer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" + integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== + +"@webassemblyjs/helper-numbers@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" + integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" + integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== + +"@webassemblyjs/helper-wasm-section@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" + integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + +"@webassemblyjs/ieee754@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" + integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" + integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" + integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== + +"@webassemblyjs/wasm-edit@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" + integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-wasm-section" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-opt" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/wast-printer" "1.11.1" + +"@webassemblyjs/wasm-gen@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" + integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wasm-opt@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" + integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + +"@webassemblyjs/wasm-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" + integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wast-printer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" + integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accept-language-parser@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/accept-language-parser/-/accept-language-parser-1.5.0.tgz#8877c54040a8dcb59e0a07d9c1fde42298334791" + integrity sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw== + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-assertions@^1.7.6: + version "1.8.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" + integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1, acorn@^8.5.0: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== + +acorn@^8.7.1, acorn@^8.8.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" + integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== + +adler-32@~1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.3.1.tgz#1dbf0b36dda0012189a32b3679061932df1821e2" + integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A== + +ajv-formats@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@8.11.0, ajv@^8.0.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ajv@8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + +ansi-colors@4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +anymatch@^3.0.3, anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-includes@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" + integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" + is-string "^1.0.7" + +array-timsort@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-timsort/-/array-timsort-1.0.3.tgz#3c9e4199e54fb2b9c3fe5976396a21614ef0d926" + integrity sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.flat@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" + integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" + integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +async@^3.2.3: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + +babel-jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.5.0.tgz#3fe3ddb109198e78b1c88f9ebdecd5e4fc2f50a5" + integrity sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q== + dependencies: + "@jest/transform" "^29.5.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.5.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz#a97db437936f441ec196990c9738d4b88538618a" + integrity sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz#57bc8cc88097af7ff6a5ab59d1cd29d52a5916e2" + integrity sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg== + dependencies: + babel-plugin-jest-hoist "^29.5.0" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +bcryptjs@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" + integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bowser@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" + integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== + +boxen@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" + integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^6.2.0" + chalk "^4.1.0" + cli-boxes "^2.2.1" + string-width "^4.2.2" + type-fest "^0.20.2" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.14.5, browserslist@^4.20.2: + version "4.21.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.1.tgz#c9b9b0a54c7607e8dc3e01a0d311727188011a00" + integrity sha512-Nq8MFCSrnJXSc88yliwlzQe3qNe3VntIjhsArW9IJOEPSHNx23FalwApUVbzAWABLhYJJ7y8AynWI/XM8OdfjQ== + dependencies: + caniuse-lite "^1.0.30001359" + electron-to-chromium "^1.4.172" + node-releases "^2.0.5" + update-browserslist-db "^1.0.4" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +bson@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/bson/-/bson-5.0.1.tgz#4cd3eeeabf6652ef0d6ab600f9a18212d39baac3" + integrity sha512-y09gBGusgHtinMon/GVbv1J6FrXhnr/+6hqLlSmEFzkz6PodqF6TxjyvfvY3AfO+oG1mgUtbC86xSbOlwvM62Q== + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +busboy@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0, callsites@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001359: + version "1.0.30001363" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001363.tgz#26bec2d606924ba318235944e1193304ea7c4f15" + integrity sha512-HpQhpzTGGPVMnCjIomjt+jvyUu8vNFo3TaDiZ/RcoTrlOq/5+tC8zHdsbgFB6MxmaY+jCpsH09aD80Bb4Ow3Sg== + +cfb@~1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44" + integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA== + dependencies: + adler-32 "~1.3.0" + crc-32 "~1.2.0" + +chalk@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== + +check-disk-space@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/check-disk-space/-/check-disk-space-3.3.1.tgz#10c4c8706fdd16d3e5c3572a16aa95efd0b4d40b" + integrity sha512-iOrT8yCZjSnyNZ43476FE2rnssvgw5hnuwOM0hm8Nj1qa0v4ieUUEbCyxxsEliaoDUb/75yCOL71zkDiDBLbMQ== + +chokidar@3.5.3, chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +ci-info@^3.2.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.2.tgz#6d2967ffa407466481c6c90b6e16b3098f080128" + integrity sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg== + +cjs-module-lexer@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" + integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== + +class-transformer@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" + integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== + +class-validator@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.0.tgz#40ed0ecf3c83b2a8a6a320f4edb607be0f0df159" + integrity sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A== + dependencies: + "@types/validator" "^13.7.10" + libphonenumber-js "^1.10.14" + validator "^13.7.0" + +clear-module@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/clear-module/-/clear-module-4.1.2.tgz#5a58a5c9f8dccf363545ad7284cad3c887352a80" + integrity sha512-LWAxzHqdHsAZlPlEyJ2Poz6AIs384mPeqLVCru2p0BrP9G/kVGuhNyZYClLO6cXlnuJjzC8xtsJIuMjKqLXoAw== + dependencies: + parent-module "^2.0.0" + resolve-from "^5.0.0" + +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" + integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== + +cli-table3@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" + integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +code-block-writer@^11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-11.0.1.tgz#53920acb587af3f7be57101c0248d98f372d73ac" + integrity sha512-0ch9DeCY8v/BWA9n1/Qu1ALG3lpesel4PYL2eNlGLgvGl+J7k74i+dSXSF3wLvF5SYII8/GUT/Ic+fycBR/DUQ== + +codepage@~1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.15.0.tgz#2e00519024b39424ec66eeb3ec07227e692618ab" + integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA== + +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + +color-convert@^1.9.0, color-convert@^1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.6.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.1.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + +colorspace@1.1.x: + version "1.1.4" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" + integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== + dependencies: + color "^3.1.3" + text-hex "1.0.x" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +commander@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.0.tgz#71797971162cd3cf65f0b9d24eb28f8d303acdf1" + integrity sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +comment-json@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.2.3.tgz#50b487ebbf43abe44431f575ebda07d30d015365" + integrity sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw== + 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" + +component-emitter@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + +consola@^2.15.0: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0, cookie@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +cookiejar@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" + integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== + +core-util-is@^1.0.3, core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cosmiconfig@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +cosmiconfig@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.1.0.tgz#947e174c796483ccf0a48476c24e4fefb7e1aea8" + integrity sha512-0tLZ9URlPGU7JsKq0DQOQ3FoRsYX8xDZ7xMiATQfaiGMz7EHowNkbU9u1coAOmnh9p/1ySpm0RB3JNWRXM5GCg== + dependencies: + import-fresh "^3.2.1" + js-yaml "^4.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + +crc-32@~1.2.0, crc-32@~1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cron@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cron/-/cron-2.2.0.tgz#605ae5048e9715a22db12e9fe2563a92740b9fed" + integrity sha512-GPiI3OgMv83XRtEUc2gUdaLvJhO3XbLN288layOBkDTupg0RK5IECNGpkykIMHg+muVR2bxt29b0xvCAcBrjYQ== + dependencies: + luxon "^3.2.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== + +crypto-js@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" + integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +cspell-dictionary@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell-dictionary/-/cspell-dictionary-6.30.0.tgz#ac2cfefa8f1db728a68ec6811d08c5296ba84b29" + integrity sha512-PY25Lrc3VC3UOtfpelBuwGilMWRD0zGa56uF9Gw+S1eAsa1eYug+Jz7/vAAUNJI91BCmF0dV9RU2LKlPNPxwaA== + dependencies: + "@cspell/cspell-pipe" "6.30.0" + "@cspell/cspell-types" "6.30.0" + cspell-trie-lib "6.30.0" + fast-equals "^4.0.3" + gensequence "^5.0.2" + +cspell-gitignore@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell-gitignore/-/cspell-gitignore-6.30.0.tgz#f7bb3ebd78e2b586b6f44d4da09f52f2126e4939" + integrity sha512-nPW7x1y4LsVgcC4dO5Tt7BKd1lXj/rul4RRoy7RQVORn/bnzOkpSeLaFp9q+4lGeC2I+p8i8brZImanLLftZ1w== + dependencies: + cspell-glob "6.30.0" + find-up "^5.0.0" + +cspell-glob@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell-glob/-/cspell-glob-6.30.0.tgz#ca9613182ad312b87e6aed6419844e24a35427e5" + integrity sha512-FHlRxn7olHiks/LMoPadL5kPuu+aPcpm9OPIF5dF/comMEzQrOG7SA6AoTuQmD7pIUv+94zzjAKOmwEfz6/fvA== + dependencies: + micromatch "^4.0.5" + +cspell-grammar@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell-grammar/-/cspell-grammar-6.30.0.tgz#dbf81502a14db6649a7ad686a5762a8952201eac" + integrity sha512-vOM5VpSknf6HNoUqqaNK47Aez1ORTPC9r6vEyjC1Uh9t6R5jGUCm0CdKI+ABKiEdPi44IETrC7lpFLIdGmQziQ== + dependencies: + "@cspell/cspell-pipe" "6.30.0" + "@cspell/cspell-types" "6.30.0" + +cspell-io@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell-io/-/cspell-io-6.30.0.tgz#851458f007eed4c776ac8b88b653171f3ccce826" + integrity sha512-y8XtfjPYBvzf87y6zsFiKuAYx/mKbWk9ACZc4Tt3pvDmGkf3anGjLJLfg6360ObGymuYwT4Jt2Al+gWsDpE6vQ== + dependencies: + "@cspell/cspell-service-bus" "6.30.0" + node-fetch "^2.6.9" + +cspell-lib@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell-lib/-/cspell-lib-6.30.0.tgz#daefa8bfd08c5045428f876c7c739328c0a9b87b" + integrity sha512-88wuGw93dICcKD2Zu9w1XvwAj5AQ/EzXleo5Eab+6G2zUjHk0zXCJorivrWXWuwnN8WePAp4mKoVUI+eXpDksw== + dependencies: + "@cspell/cspell-bundled-dicts" "6.30.0" + "@cspell/cspell-pipe" "6.30.0" + "@cspell/cspell-types" "6.30.0" + "@cspell/strong-weak-map" "6.30.0" + clear-module "^4.1.2" + comment-json "^4.2.3" + configstore "^5.0.1" + cosmiconfig "^8.1.0" + cspell-dictionary "6.30.0" + cspell-glob "6.30.0" + cspell-grammar "6.30.0" + cspell-io "6.30.0" + cspell-trie-lib "6.30.0" + fast-equals "^4.0.3" + find-up "^5.0.0" + gensequence "^5.0.2" + import-fresh "^3.3.0" + resolve-from "^5.0.0" + resolve-global "^1.0.0" + vscode-languageserver-textdocument "^1.0.8" + vscode-uri "^3.0.7" + +cspell-trie-lib@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell-trie-lib/-/cspell-trie-lib-6.30.0.tgz#1e1aa4fdaa0b894b1418c6214ea13dc826a37180" + integrity sha512-drl5vSm7JJvlchxfd4tYR9EGKj4a1ga2oqqKKPq7oh9BukN3UW03LdYYjjn3ou1By8bnZHvTSy1M06rHrtNxmA== + dependencies: + "@cspell/cspell-pipe" "6.30.0" + "@cspell/cspell-types" "6.30.0" + gensequence "^5.0.2" + +cspell@^6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/cspell/-/cspell-6.30.0.tgz#5bd7fd8bbc5c864b7c22ba834f7531dfb408587a" + integrity sha512-jv89wDPPhXMVVyzOiQWRq2458QnoUaVrse2oXWky13UNVcHrsXq5GS4gQtKjAVxvwSJcVZpwtN59eyulrqAFpw== + dependencies: + "@cspell/cspell-pipe" "6.30.0" + "@cspell/dynamic-import" "6.30.0" + chalk "^4.1.2" + commander "^10.0.0" + cspell-gitignore "6.30.0" + cspell-glob "6.30.0" + cspell-io "6.30.0" + cspell-lib "6.30.0" + fast-glob "^3.2.12" + fast-json-stable-stringify "^2.1.0" + file-entry-cache "^6.0.1" + get-stdin "^8.0.0" + imurmurhash "^0.1.4" + semver "^7.3.8" + strip-ansi "^6.0.1" + vscode-uri "^3.0.7" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4.x, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +defaults@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + integrity sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA== + dependencies: + clone "^1.0.2" + +define-properties@^1.1.3, define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + +diff-sequences@^29.0.0: + version "29.0.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.0.0.tgz#bae49972ef3933556bcb0800b72e8579d19d9e4f" + integrity sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA== + +diff-sequences@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" + integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dot-prop@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +dotenv-expand@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" + integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== + +dotenv@16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" + integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.172: + version "1.4.182" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.182.tgz#5d59214ebfe90b36f23e81cd226a42732cd8c677" + integrity sha512-OpEjTADzGoXABjqobGhpy0D2YsTncAax7IkER68ycc4adaq0dqEG9//9aenKPy7BGA90bqQdLac0dPp6uMkcSg== + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0, enhanced-resolve@^5.7.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6" + integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.19.0, es-abstract@^1.19.5: + version "1.20.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" + integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + regexp.prototype.flags "^1.4.3" + string.prototype.trimend "^1.0.5" + string.prototype.trimstart "^1.0.5" + unbox-primitive "^1.0.2" + +es-abstract@^1.20.4: + version "1.21.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.1.tgz#e6105a099967c08377830a0c9cb589d570dd86c6" + integrity sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.3" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.4" + is-array-buffer "^3.0.1" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.10" + is-weakref "^1.0.2" + object-inspect "^1.12.2" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.9" + +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== + +es-set-tostringtag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" + integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + has-tostringtag "^1.0.0" + +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-prettier@^8.7.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.7.0.tgz#f1cc58a8afebc50980bd53475451df146c13182d" + integrity sha512-HHVXLSlVUhMSmyW4ZzEuvjpwqamgmlfkutD53cYXLikh4pt/modINRcCIApJ84czDxM4GZInwUrromsDdTImTA== + +eslint-import-resolver-node@^0.3.7: + version "0.3.7" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz#83b375187d412324a1963d84fa664377a23eb4d7" + integrity sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA== + dependencies: + debug "^3.2.7" + is-core-module "^2.11.0" + resolve "^1.22.1" + +eslint-module-utils@^2.7.4: + version "2.7.4" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" + integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@^2.27.5: + version "2.27.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65" + integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + array.prototype.flatmap "^1.3.1" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.7" + eslint-module-utils "^2.7.4" + has "^1.0.3" + is-core-module "^2.11.0" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.values "^1.1.6" + resolve "^1.22.1" + semver "^6.3.0" + tsconfig-paths "^3.14.1" + +eslint-scope@5.1.1, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@^8.36.0: + version "8.36.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.36.0.tgz#1bd72202200a5492f91803b113fb8a83b11285cf" + integrity sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.4.0" + "@eslint/eslintrc" "^2.0.1" + "@eslint/js" "8.36.0" + "@humanwhocodes/config-array" "^0.11.8" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-visitor-keys "^3.3.0" + espree "^9.5.0" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + 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.1" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +espree@^9.5.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.0.tgz#3646d4e3f58907464edba852fa047e6a27bdf113" + integrity sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.2.tgz#c6d3fee05dd665808e2ad870631f221f5617b1d1" + integrity sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.0.0: + version "29.0.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.0.1.tgz#a2fa64a59cffe4b4007877e730bc82be3d1742bb" + integrity sha512-yQgemsjLU+1S8t2A7pXT3Sn/v5/37LY8J+tocWtKEA0iEYYc6gfKbbJJX2fxHZmd7K9WpdbQqXUpmYkq1aewYg== + dependencies: + "@jest/expect-utils" "^29.0.1" + jest-get-type "^29.0.0" + jest-matcher-utils "^29.0.1" + jest-message-util "^29.0.1" + jest-util "^29.0.1" + +expect@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.5.0.tgz#68c0509156cb2a0adb8865d413b137eeaae682f7" + integrity sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg== + dependencies: + "@jest/expect-utils" "^29.5.0" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.5.0" + jest-message-util "^29.5.0" + jest-util "^29.5.0" + +express@4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-equals@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-4.0.3.tgz#72884cc805ec3c6679b99875f6b7654f39f0e8c7" + integrity sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg== + +fast-glob@^3.2.12: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-glob@^3.2.7, fast-glob@^3.2.9: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + +fast-xml-parser@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.1.2.tgz#5a98c18238d28a57bbdfa9fe4cda01211fff8f4a" + integrity sha512-CDYeykkle1LiA/uqQyNwYpFbyF6Axec6YapmpUP+/RHWIoR1zKjocdvNaTsxCxZzQ6v9MLXaSYm9Qq0thv0DHg== + dependencies: + strnum "^1.0.5" + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +fb-watchman@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + dependencies: + bser "2.1.1" + +fecha@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" + integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== + +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +file-stream-rotator@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz#007019e735b262bb6c6f0197e58e5c87cb96cec3" + integrity sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ== + dependencies: + moment "^2.29.1" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.6.tgz#022e9218c637f9f3fc9c35ab9c9193f05add60b2" + integrity sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ== + +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +fork-ts-checker-webpack-plugin@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.3.0.tgz#a9c984a018493962360d7c7e77a67b44a2d5f3aa" + integrity sha512-IN+XTzusCjR5VgntYFgxbxVx3WraPRnKehBFrf00cMSrtUuW9MsG9dhL6MWpY6MkjC3wVwoujfCDgZZCQwbswA== + dependencies: + "@babel/code-frame" "^7.16.7" + chalk "^4.1.2" + chokidar "^3.5.3" + cosmiconfig "^7.0.1" + deepmerge "^4.2.2" + fs-extra "^10.0.0" + memfs "^3.4.1" + minimatch "^3.0.4" + node-abort-controller "^3.0.1" + schema-utils "^3.1.1" + semver "^7.3.5" + tapable "^2.2.1" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formidable@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.1.tgz#81269cbea1a613240049f5f61a9d97731517414f" + integrity sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ== + dependencies: + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +frac@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b" + integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@10.1.0, fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.0.tgz#5784b102104433bb0e090f48bfc4a30742c357ed" + integrity sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-monkey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" + integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^2.3.2, fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gensequence@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/gensequence/-/gensequence-5.0.2.tgz#f065be2f9a5b2967b9cad7f33b2d79ce1f22dc82" + integrity sha512-JlKEZnFc6neaeSVlkzBGGgkIoIaSxMgvdamRoPN8r3ozm2r9dusqxeKqYQ7lhzmj2UhFQP8nkyfCaiLQxiLrDA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +geolib@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/geolib/-/geolib-3.3.3.tgz#17f5a0dcdc0b051bd631b66f7131d2c14c54a15b" + integrity sha512-YO704pzdB/8QQekQuDmFD5uv5RAwAf4rOUPdcMhdEOz+HoPWD0sC7Qqdwb+LAvwIjXVRawx0QgZlocKYh8PFOQ== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" + integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-intrinsic@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-stdin@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" + integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== + +get-stream@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.0.0, glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^9.2.0: + version "9.2.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.2.1.tgz#f47e34e1119e7d4f93a546e75851ba1f1e68de50" + integrity sha512-Pxxgq3W0HyA3XUvSXcFhRSs+43Jsx0ddxcFrbjxNGkL2Ak5BAUBxLqI5G6ADDeCHLfzzXFhe0b1yYcctGmytMA== + dependencies: + fs.realpath "^1.0.0" + minimatch "^7.4.1" + minipass "^4.2.4" + path-scurry "^1.6.1" + +global-dirs@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" + integrity sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg== + dependencies: + ini "^1.3.4" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.19.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.19.0.tgz#7a42de8e6ad4f7242fbcca27ea5b23aca367b5c8" + integrity sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ== + dependencies: + type-fest "^0.20.2" + +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-own-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-own-prop/-/has-own-prop-2.0.0.tgz#f0f95d58f65804f5d218db32563bb85b8e0417af" + integrity sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ== + +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + +has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +helmet@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-6.0.1.tgz#52ec353638b2e87f14fe079d142b368ac11e79a4" + integrity sha512-8wo+VdQhTMVBMCITYZaGTbE4lvlthelPYSvoyNvk4RECTmrVjMerp9RfUOQXZWLvCcAn1pKj7ZRxK4lI9Alrcw== + +hexoid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +husky@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" + integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== + +iconv-lite@0.4.24, iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + +import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +import-meta-resolve@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-2.2.2.tgz#75237301e72d1f0fbd74dbc6cca9324b164c2cc9" + integrity sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.4: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +inquirer@7.3.3: + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.19" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.6.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +inquirer@8.2.4: + version "8.2.4" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" + integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.5.5" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + wrap-ansi "^7.0.0" + +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + +internal-slot@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" + integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + side-channel "^1.0.4" + +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + +ip@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" + integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-array-buffer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a" + integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-typed-array "^1.1.10" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.3, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-callable@^1.1.4, is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + +is-core-module@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + dependencies: + has "^1.0.3" + +is-core-module@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== + dependencies: + has "^1.0.3" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.10, is-typed-array@^1.1.9: + version "1.1.10" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" + integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz#31d18bdd127f825dd02ea7bfdfd906f8ab840e9f" + integrity sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.4.tgz#1b6f068ecbc6c331040aab5741991273e609e40c" + integrity sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +iterare@1.2.1, iterare@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042" + integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q== + +jest-changed-files@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" + integrity sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag== + dependencies: + execa "^5.0.0" + p-limit "^3.1.0" + +jest-circus@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.5.0.tgz#b5926989449e75bff0d59944bae083c9d7fb7317" + integrity sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/expect" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + is-generator-fn "^2.0.0" + jest-each "^29.5.0" + jest-matcher-utils "^29.5.0" + jest-message-util "^29.5.0" + jest-runtime "^29.5.0" + jest-snapshot "^29.5.0" + jest-util "^29.5.0" + p-limit "^3.1.0" + pretty-format "^29.5.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.5.0.tgz#b34c20a6d35968f3ee47a7437ff8e53e086b4a67" + integrity sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw== + dependencies: + "@jest/core" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/types" "^29.5.0" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + import-local "^3.0.2" + jest-config "^29.5.0" + jest-util "^29.5.0" + jest-validate "^29.5.0" + prompts "^2.0.1" + yargs "^17.3.1" + +jest-config@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.5.0.tgz#3cc972faec8c8aaea9ae158c694541b79f3748da" + integrity sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.5.0" + "@jest/types" "^29.5.0" + babel-jest "^29.5.0" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.5.0" + jest-environment-node "^29.5.0" + jest-get-type "^29.4.3" + jest-regex-util "^29.4.3" + jest-resolve "^29.5.0" + jest-runner "^29.5.0" + jest-util "^29.5.0" + jest-validate "^29.5.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.5.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.0.1.tgz#d14e900a38ee4798d42feaaf0c61cb5b98e4c028" + integrity sha512-l8PYeq2VhcdxG9tl5cU78ClAlg/N7RtVSp0v3MlXURR0Y99i6eFnegmasOandyTmO6uEdo20+FByAjBFEO9nuw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.0.0" + jest-get-type "^29.0.0" + pretty-format "^29.0.1" + +jest-diff@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.5.0.tgz#e0d83a58eb5451dcc1fa61b1c3ee4e8f5a290d63" + integrity sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.4.3" + jest-get-type "^29.4.3" + pretty-format "^29.5.0" + +jest-docblock@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8" + integrity sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.5.0.tgz#fc6e7014f83eac68e22b7195598de8554c2e5c06" + integrity sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA== + dependencies: + "@jest/types" "^29.5.0" + chalk "^4.0.0" + jest-get-type "^29.4.3" + jest-util "^29.5.0" + pretty-format "^29.5.0" + +jest-environment-node@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.5.0.tgz#f17219d0f0cc0e68e0727c58b792c040e332c967" + integrity sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/fake-timers" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + jest-mock "^29.5.0" + jest-util "^29.5.0" + +jest-get-type@^29.0.0: + version "29.0.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.0.0.tgz#843f6c50a1b778f7325df1129a0fd7aa713aef80" + integrity sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw== + +jest-get-type@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" + integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== + +jest-haste-map@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.5.0.tgz#69bd67dc9012d6e2723f20a945099e972b2e94de" + integrity sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA== + dependencies: + "@jest/types" "^29.5.0" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.4.3" + jest-util "^29.5.0" + jest-worker "^29.5.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz#cf4bdea9615c72bac4a3a7ba7e7930f9c0610c8c" + integrity sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow== + dependencies: + jest-get-type "^29.4.3" + pretty-format "^29.5.0" + +jest-matcher-utils@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.0.1.tgz#eaa92dd5405c2df9d31d45ec4486361d219de3e9" + integrity sha512-/e6UbCDmprRQFnl7+uBKqn4G22c/OmwriE5KCMVqxhElKCQUDcFnq5XM9iJeKtzy4DUjxT27y9VHmKPD8BQPaw== + dependencies: + chalk "^4.0.0" + jest-diff "^29.0.1" + jest-get-type "^29.0.0" + pretty-format "^29.0.1" + +jest-matcher-utils@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz#d957af7f8c0692c5453666705621ad4abc2c59c5" + integrity sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw== + dependencies: + chalk "^4.0.0" + jest-diff "^29.5.0" + jest-get-type "^29.4.3" + pretty-format "^29.5.0" + +jest-message-util@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.0.1.tgz#85c4b5b90296c228da158e168eaa5b079f2ab879" + integrity sha512-wRMAQt3HrLpxSubdnzOo68QoTfQ+NLXFzU0Heb18ZUzO2S9GgaXNEdQ4rpd0fI9dq2NXkpCk1IUWSqzYKji64A== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.0.1" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.0.1" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-message-util@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.5.0.tgz#1f776cac3aca332ab8dd2e3b41625435085c900e" + integrity sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.5.0" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.5.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.5.0.tgz#26e2172bcc71d8b0195081ff1f146ac7e1518aed" + integrity sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw== + dependencies: + "@jest/types" "^29.5.0" + "@types/node" "*" + jest-util "^29.5.0" + +jest-pnp-resolver@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" + integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== + +jest-regex-util@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.3.tgz#a42616141e0cae052cfa32c169945d00c0aa0bb8" + integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg== + +jest-resolve-dependencies@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz#f0ea29955996f49788bf70996052aa98e7befee4" + integrity sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg== + dependencies: + jest-regex-util "^29.4.3" + jest-snapshot "^29.5.0" + +jest-resolve@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.5.0.tgz#b053cc95ad1d5f6327f0ac8aae9f98795475ecdc" + integrity sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.5.0" + jest-pnp-resolver "^1.2.2" + jest-util "^29.5.0" + jest-validate "^29.5.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.5.0.tgz#6a57c282eb0ef749778d444c1d758c6a7693b6f8" + integrity sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ== + dependencies: + "@jest/console" "^29.5.0" + "@jest/environment" "^29.5.0" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.4.3" + jest-environment-node "^29.5.0" + jest-haste-map "^29.5.0" + jest-leak-detector "^29.5.0" + jest-message-util "^29.5.0" + jest-resolve "^29.5.0" + jest-runtime "^29.5.0" + jest-util "^29.5.0" + jest-watcher "^29.5.0" + jest-worker "^29.5.0" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.5.0.tgz#c83f943ee0c1da7eb91fa181b0811ebd59b03420" + integrity sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw== + dependencies: + "@jest/environment" "^29.5.0" + "@jest/fake-timers" "^29.5.0" + "@jest/globals" "^29.5.0" + "@jest/source-map" "^29.4.3" + "@jest/test-result" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.5.0" + jest-message-util "^29.5.0" + jest-mock "^29.5.0" + jest-regex-util "^29.4.3" + jest-resolve "^29.5.0" + jest-snapshot "^29.5.0" + jest-util "^29.5.0" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.5.0.tgz#c9c1ce0331e5b63cd444e2f95a55a73b84b1e8ce" + integrity sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/traverse" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.5.0" + "@jest/transform" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/babel__traverse" "^7.0.6" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.5.0" + graceful-fs "^4.2.9" + jest-diff "^29.5.0" + jest-get-type "^29.4.3" + jest-matcher-utils "^29.5.0" + jest-message-util "^29.5.0" + jest-util "^29.5.0" + natural-compare "^1.4.0" + pretty-format "^29.5.0" + semver "^7.3.5" + +jest-util@^29.0.0: + version "29.0.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.0.2.tgz#c75c5cab7f3b410782f9570a60c5558b5dfb6e3a" + integrity sha512-ozk8ruEEEACxqpz0hN9UOgtPZS0aN+NffwQduR5dVlhN+eN47vxurtvgZkYZYMpYrsmlAEx1XabkB3BnN0GfKQ== + dependencies: + "@jest/types" "^29.0.2" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-util@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.0.1.tgz#f854a4a8877c7817316c4afbc2a851ceb2e71598" + integrity sha512-GIWkgNfkeA9d84rORDHPGGTFBrRD13A38QVSKE0bVrGSnoR1KDn8Kqz+0yI5kezMgbT/7zrWaruWP1Kbghlb2A== + dependencies: + "@jest/types" "^29.0.1" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-util@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f" + integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ== + dependencies: + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.5.0.tgz#8e5a8f36178d40e47138dc00866a5f3bd9916ffc" + integrity sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ== + dependencies: + "@jest/types" "^29.5.0" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.4.3" + leven "^3.1.0" + pretty-format "^29.5.0" + +jest-watcher@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.5.0.tgz#cf7f0f949828ba65ddbbb45c743a382a4d911363" + integrity sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA== + dependencies: + "@jest/test-result" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.5.0" + string-length "^4.0.1" + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest-worker@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.5.0.tgz#bdaefb06811bd3384d93f009755014d8acb4615d" + integrity sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA== + dependencies: + "@types/node" "*" + jest-util "^29.5.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.5.0.tgz#f75157622f5ce7ad53028f2f8888ab53e1f1f24e" + integrity sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ== + dependencies: + "@jest/core" "^29.5.0" + "@jest/types" "^29.5.0" + import-local "^3.0.2" + jest-cli "^29.5.0" + +joi@^17.8.4: + version "17.8.4" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.8.4.tgz#f2d91ab8acd3cca4079ba70669c65891739234aa" + integrity sha512-jjdRHb5WtL+KgSHvOULQEPPv4kcl+ixd1ybOFQq3rWLgEEqc03QMmilodL0GVJE14U/SQDXkUhQUSZANGDH/AA== + dependencies: + "@hapi/hoek" "^9.0.0" + "@hapi/topo" "^5.0.0" + "@sideway/address" "^4.1.3" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" + +js-sdsl@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.4.tgz#78793c90f80e8430b7d8dc94515b6c77d98a26a6" + integrity sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +json5@^2.1.3, json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + +json5@^2.2.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonc-parser@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + +jsonc-parser@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" + integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonwebtoken@9.0.0, jsonwebtoken@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d" + integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw== + dependencies: + jws "^3.2.2" + lodash "^4.17.21" + ms "^2.1.1" + semver "^7.3.8" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +kareem@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.5.1.tgz#7b8203e11819a8e77a34b3517d3ead206764d15d" + integrity sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +libphonenumber-js@^1.10.14: + version "1.10.15" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.15.tgz#cad454adb5bf271bc820bbf7dd66776afcda7be6" + integrity sha512-sLeVLmWX17VCKKulc+aDIRHS95TxoTsKMRJi5s5gJdwlqNzMWcBCtSHHruVyXjqfi67daXM2SnLf2juSrdx5Sg== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.compact@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash.compact/-/lodash.compact-3.0.1.tgz#540ce3837745975807471e16b4a2ba21e7256ca5" + integrity sha512-2ozeiPi+5eBXW1CLtzjk8XQFhQOEMwwfxblqeq6EGyTxZJ1bPATqilY0e6g2SLQpP4KuMeuioBhEnWz5Pr7ICQ== + +lodash.flattendeep@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" + integrity sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ== + +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +logform@^2.3.2, logform@^2.4.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.4.2.tgz#a617983ac0334d0c3b942c34945380062795b47c" + integrity sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw== + dependencies: + "@colors/colors" "1.5.0" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^2.3.1" + triple-beam "^1.3.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lru-cache@^7.14.1: + version "7.18.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.1.tgz#4716408dec51d5d0104732647f584d1f6738b109" + integrity sha512-8/HcIENyQnfUTCDizRu9rrDyG6XG/21M4X7/YEGZeD76ZJilFPAUVb/2zysFf7VVO1LEjCDFyHp8pMMvozIrvg== + +luxon@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.2.1.tgz#14f1af209188ad61212578ea7e3d518d18cee45f" + integrity sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg== + +macos-release@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.0.tgz#067c2c88b5f3fb3c56a375b2ec93826220fa1ff2" + integrity sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g== + +magic-string@0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.1.tgz#ba9b651354fa9512474199acecf9c6dbe93f97fd" + integrity sha512-ndThHmvgtieXe8J/VGPjG+Apu7v7ItcD5mhEIvOscWjPF/ccOiLxHaSuCAS2G+3x4GKsAbT8u7zdyamupui8Tg== + dependencies: + sourcemap-codec "^1.4.8" + +magic-string@0.26.7: + version "0.26.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.7.tgz#caf7daf61b34e9982f8228c4527474dac8981d6f" + integrity sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow== + dependencies: + sourcemap-codec "^1.4.8" + +magic-string@0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" + integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.13" + +make-dir@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-error@1.x, make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +md5@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.7.tgz#e5252ad2242a724f938cb937e3c4f7ceb1f70e5a" + integrity sha512-ygaiUSNalBX85388uskeCyhSAoOSgzBbtVCr9jA2RROssFL9Q19/ZXFqS+2Th2sr1ewNIWgFdLzLC3Yl1Zv+lw== + dependencies: + fs-monkey "^1.0.3" + +memory-pager@^1.0.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@^1.1.2, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.0, micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^7.4.1: + version "7.4.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.2.tgz#157e847d79ca671054253b840656720cb733f10f" + integrity sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +minipass@^4.0.2, minipass@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.4.tgz#7d0d97434b6a19f59c5c3221698b48bbf3b2cd06" + integrity sha512-lwycX3cBMTvcejsHITUgYj6Gy6A7Nh4Q6h9NP4sTHY1ccJlC7yKzDmiShEHsJ16Jf1nKGDEaiHxiltsJEvk0nQ== + +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +moment@2.x.x, moment@^2.29.1, moment@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + +mongodb-connection-string-url@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz#57901bf352372abdde812c81be47b75c6b2ec5cf" + integrity sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ== + dependencies: + "@types/whatwg-url" "^8.2.1" + whatwg-url "^11.0.0" + +mongodb@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.1.0.tgz#e551f9e496777bde9173e51d16c163ab2c805b9d" + integrity sha512-qgKb7y+EI90y4weY3z5+lIgm8wmexbonz0GalHkSElQXVKtRuwqXuhXKccyvIjXCJVy9qPV82zsinY0W1FBnJw== + dependencies: + bson "^5.0.1" + mongodb-connection-string-url "^2.6.0" + socks "^2.7.1" + optionalDependencies: + saslprep "^1.0.3" + +mongoose@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.0.2.tgz#679df6bae18abb7a27f412c4e9e7285363cd07eb" + integrity sha512-whX+5lAOLOs6VXRr9w+6m5qb8m/IXWLLb9+0/HRUh2TiIYtTt7UvajK92zW6wllCjBkrrnz/MDIOTCWMbs8K4g== + dependencies: + bson "^5.0.1" + kareem "2.5.1" + mongodb "5.1.0" + mpath "0.9.0" + mquery "5.0.0" + ms "2.1.3" + sift "16.0.1" + +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + +mpath@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.9.0.tgz#0c122fe107846e31fc58c75b09c35514b3871904" + integrity sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew== + +mquery@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-5.0.0.tgz#a95be5dfc610b23862df34a47d3e5d60e110695d" + integrity sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg== + dependencies: + debug "4.x" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multer@1.4.4-lts.1: + version "1.4.4-lts.1" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4-lts.1.tgz#24100f701a4611211cfae94ae16ea39bb314e04d" + integrity sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg== + dependencies: + append-field "^1.0.0" + busboy "^1.0.0" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + type-is "^1.6.4" + xtend "^4.0.0" + +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +nest-winston@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/nest-winston/-/nest-winston-1.9.1.tgz#2968a9553ca60728be93e47ccb659a8946e97481" + integrity sha512-L3fjIfas+7ZziKsCQiTYvTVw0Hpv3oN4TDnbLLYLIQgKLNpzQyf/2yZv1/buDPMrGJKrNPwiHLoifjGHon34+A== + dependencies: + fast-safe-stringify "^2.1.1" + +nestjs-command@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/nestjs-command/-/nestjs-command-3.1.3.tgz#98149d00d32232a2d24cc22f12c13b856753a784" + integrity sha512-RWPk9q4Z14KoLReLJz/ehQ/60ezCxCJc2ko4DnuUDJ3w30ZbtNVEE7CRHfbbl5QepSHDBj8ydJreHVGA73YKng== + dependencies: + lodash.compact "^3.0.1" + lodash.flattendeep "^4.4.0" + +nestjs-i18n@^10.2.6: + version "10.2.6" + resolved "https://registry.yarnpkg.com/nestjs-i18n/-/nestjs-i18n-10.2.6.tgz#ea15e863731036ddd5f4eb5068c56dab1355ebdc" + integrity sha512-vWjOXNz3ohJcKybtgdCWruNqreNOkVGfbTzGFxxdZ6Y9VfVJERT2+/30KAcBwJTpjuStoTVhMpILAKkXMk8KLQ== + dependencies: + accept-language-parser "^1.5.0" + chokidar "^3.5.3" + cookie "^0.5.0" + iterare "^1.2.1" + js-yaml "^4.1.0" + string-format "^2.0.0" + +node-abort-controller@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e" + integrity sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw== + +node-emoji@1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" + integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== + dependencies: + lodash "^4.17.21" + +node-fetch@^2.6.1: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" + integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== + dependencies: + whatwg-url "^5.0.0" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666" + integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.0, npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-assign@^4, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-hash@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + +object-inspect@^1.12.0, object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +object-inspect@^1.12.2: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.values@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" + integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1, on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +ora@5.4.1, ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + +os-name@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/os-name/-/os-name-4.0.1.tgz#32cee7823de85a8897647ba4d76db46bf845e555" + integrity sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw== + dependencies: + macos-release "^2.5.0" + windows-release "^4.0.0" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parent-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-2.0.0.tgz#fa71f88ff1a50c27e15d8ff74e0e3a9523bf8708" + integrity sha512-uo0Z9JJeWzv8BG+tRcapBKNJ0dro9cLyczGzulS6EfeyAdeC9sbojtW6XwvYxJkEne9En+J2XEl4zyglVeIwFg== + dependencies: + callsites "^3.1.0" + +parse-json@^5.0.0, parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +passport-headerapikey@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/passport-headerapikey/-/passport-headerapikey-1.2.2.tgz#b71960523999c9864151b8535c919e3ff5ba75ce" + integrity sha512-4BvVJRrWsNJPrd3UoZfcnnl4zvUWYKEtfYkoDsaOKBsrWHYmzTApCjs7qUbncOLexE9ul0IRiYBFfBG0y9IVQA== + dependencies: + lodash "^4.17.15" + passport-strategy "^1.0.0" + +passport-jwt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.1.tgz#c443795eff322c38d173faa0a3c481479646ec3d" + integrity sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ== + dependencies: + jsonwebtoken "^9.0.0" + passport-strategy "^1.0.0" + +passport-strategy@1.x.x, passport-strategy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.6.0.tgz#e869579fab465b5c0b291e841e6cc95c005fac9d" + integrity sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + utils-merge "^1.0.1" + +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.6.1.tgz#dab45f7bb1d3f45a0e271ab258999f4ab7e23132" + integrity sha512-OW+5s+7cw6253Q4E+8qQ/u1fVvcJQCJo/VFD8pje+dbJCF1n5ZRMV2AEHbGp+5Q7jxQIYJxkHopnj6nzdGeZLA== + dependencies: + lru-cache "^7.14.1" + minipass "^4.0.2" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-to-regexp@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" + integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pirates@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pluralize@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier@^2.8.4: + version "2.8.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" + integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== + +pretty-format@^29.0.0, pretty-format@^29.0.1: + version "29.0.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.0.1.tgz#2f8077114cdac92a59b464292972a106410c7ad0" + integrity sha512-iTHy3QZMzuL484mSTYbQIM1AHhEQsH8mXWS2/vd2yFBYnG3EBqGiMONo28PlPgrW7P/8s/1ISv+y7WH306l8cw== + dependencies: + "@jest/schemas" "^29.0.0" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +pretty-format@^29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.5.0.tgz#283134e74f70e2e3e7229336de0e4fce94ccde5a" + integrity sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw== + dependencies: + "@jest/schemas" "^29.4.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +pure-rand@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.1.tgz#31207dddd15d43f299fdcdb2f572df65030c19af" + integrity sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg== + +qs@6.11.0, qs@^6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +readable-stream@^2.2.2: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== + dependencies: + resolve "^1.1.6" + +reflect-metadata@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + +regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-global@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-global/-/resolve-global-1.0.0.tgz#a2a79df4af2ca3f49bf77ef9ddacd322dad19255" + integrity sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw== + dependencies: + global-dirs "^0.1.1" + +resolve.exports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.0.tgz#c1a0028c2d166ec2fbf7d0644584927e76e7400e" + integrity sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg== + +resolve@^1.1.6, resolve@^1.20.0, resolve@^1.22.1: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +response-time@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/response-time/-/response-time-2.3.2.tgz#ffa71bab952d62f7c1d49b7434355fbc68dffc5a" + integrity sha512-MUIDaDQf+CVqflfTdQ5yam+aYCkXj1PY8fjlPDQ6ppxJlmgZb864pHtA750mayywNg8tx4rS7qH9JXd/OF+3gw== + dependencies: + depd "~1.1.0" + on-headers "~1.0.1" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.1.2.tgz#20dfbc98083bdfaa28b01183162885ef213dbf7c" + integrity sha512-BlIbgFryTbw3Dz6hyoWFhKk+unCcHMSkZGrTFVAx2WmttdBSonsdtRlwiuTbDqTKr+UlXIUqJVS4QT5tUzGENQ== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rimraf@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.4.0.tgz#c7a9f45bb2ec058d2e60ef9aca5167974313d605" + integrity sha512-X36S+qpCUR0HjXlkDe4NAOhS//aHH0Z+h8Ckf2auGJk3PTnx5rLmrHkwNdbVQuCSUhOyFrlRvFEllZOYE+yZGQ== + dependencies: + glob "^9.2.0" + +rotating-file-stream@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/rotating-file-stream/-/rotating-file-stream-3.1.0.tgz#6cf50e1671de82a396de6d31d39a6f2445f45fba" + integrity sha512-TkMF6cP1/QDcon9D71mjxHoflNuznNOrY5JJQfuxkKklZRmoow/lWBLNxXVjb6KcjAU8BDCV145buLgOx9Px1Q== + +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rxjs@6.6.7, rxjs@^6.6.0: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + +rxjs@^7.5.5: + version "7.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" + integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== + dependencies: + tslib "^2.1.0" + +rxjs@^7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" + integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== + dependencies: + tslib "^2.1.0" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + +safe-stable-stringify@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73" + integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +saslprep@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" + integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== + dependencies: + sparse-bitfield "^3.0.3" + +schema-utils@^3.1.0, schema-utils@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +semver@7.x, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" + +semver@^6.0.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.3.8: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.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 "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shelljs@0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +sift@16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/sift/-/sift-16.0.1.tgz#e9c2ccc72191585008cf3e36fc447b2d2633a053" + integrity sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ== + +signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" + integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== + dependencies: + ip "^2.0.0" + smart-buffer "^4.2.0" + +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@0.5.21, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +source-map@0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ== + dependencies: + memory-pager "^1.0.2" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +ssf@~0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c" + integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g== + dependencies: + frac "~1.1.2" + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== + +stack-utils@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" + integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== + dependencies: + escape-string-regexp "^2.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +string-format@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b" + integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA== + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string.prototype.trimend@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0" + integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + +string.prototype.trimend@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" + integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string.prototype.trimstart@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef" + integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + +string.prototype.trimstart@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" + integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" + integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== + +superagent@^8.0.5: + version "8.0.6" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.6.tgz#e3fb0b3112b79b12acd605c08846253197765bf6" + integrity sha512-HqSe6DSIh3hEn6cJvCkaM1BLi466f1LHi4yubR0tpewlMpk4RUFFy35bKz8SsPBwYfIIJy5eclp+3tCYAuX0bw== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.3" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.1.1" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.0" + semver "^7.3.8" + +supertest@^6.3.3: + version "6.3.3" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.3.tgz#42f4da199fee656106fd422c094cf6c9578141db" + integrity sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA== + dependencies: + methods "^1.1.2" + superagent "^8.0.5" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +swagger-ui-dist@4.15.5: + version "4.15.5" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz#cda226a79db2a9192579cc1f37ec839398a62638" + integrity sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA== + +symbol-observable@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" + integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== + +tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.1.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz#8033db876dd5875487213e87c627bca323e5ed90" + integrity sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.7" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + terser "^5.7.2" + +terser@^5.7.2: + version "5.14.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" + integrity "sha1-msnyKwaZTXNhdPQJGqNo24lvHBA= sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==" + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tree-kill@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +triple-beam@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" + integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== + +"true-myth@^4.1.0": + version "4.1.1" + resolved "https://registry.yarnpkg.com/true-myth/-/true-myth-4.1.1.tgz#ff4ac9d5130276e34aa338757e2416ec19248ba2" + integrity sha512-rqy30BSpxPznbbTcAcci90oZ1YR4DqvKcNXNerG5gQBU2v4jk0cygheiul5J6ExIMrgDVuanv/MkGfqZbKrNNg== + +ts-jest@^29.0.5: + version "29.0.5" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.0.5.tgz#c5557dcec8fe434fcb8b70c3e21c6b143bfce066" + integrity sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^29.0.0" + json5 "^2.2.3" + lodash.memoize "4.x" + make-error "1.x" + semver "7.x" + yargs-parser "^21.0.1" + +ts-loader@^9.4.2: + version "9.4.2" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.2.tgz#80a45eee92dd5170b900b3d00abcfa14949aeb78" + integrity sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.0.0" + micromatch "^4.0.0" + semver "^7.3.4" + +ts-morph@^13.0.1: + version "13.0.3" + resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-13.0.3.tgz#c0c51d1273ae2edb46d76f65161eb9d763444c1d" + integrity sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw== + dependencies: + "@ts-morph/common" "~0.12.3" + code-block-writer "^11.0.0" + +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +ts-prune@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/ts-prune/-/ts-prune-0.10.3.tgz#b6c71a525543b38dcf947a7d3adfb7f9e8b91f38" + integrity sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw== + dependencies: + commander "^6.2.1" + cosmiconfig "^7.0.1" + json5 "^2.1.3" + lodash "^4.17.21" + "true-myth" "^4.1.0" + ts-morph "^13.0.1" + +tsconfig-paths-webpack-plugin@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.0.0.tgz#84008fc3e3e0658fdb0262758b07b4da6265ff1a" + integrity sha512-fw/7265mIWukrSHd0i+wSwx64kYUSAKPfxRDksjKIYTxSAp9W9/xcZVBF4Kl0eqQd5eBpAQ/oQrc5RyM/0c1GQ== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.7.0" + tsconfig-paths "^4.0.0" + +tsconfig-paths@4.1.2, tsconfig-paths@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.1.2.tgz#4819f861eef82e6da52fb4af1e8c930a39ed979a" + integrity sha512-uhxiMgnXQp1IR622dUXI+9Ehnws7i/y6xvpZB9IbUVOPy0muvdvgXeZOn88UcGPiT98Vp3rJPTa8bFoalZ3Qhw== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tsconfig-paths@^3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tsconfig-paths@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.1.0.tgz#f8ef7d467f08ae3a695335bf1ece088c5538d2c1" + integrity sha512-AHx4Euop/dXFC+Vx589alFba8QItjF+8hf8LtmuiCwHyI4rHXQtOOENaM8kvYf5fR0dRChy3wzWIZ9WbB7FWow== + dependencies: + json5 "^2.2.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + +tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.1.0, tslib@^2.3.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-is@^1.6.4, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +typescript@4.9.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + +typescript@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.2.tgz#891e1a90c5189d8506af64b9ef929fca99ba1ee5" + integrity sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw== + +ua-parser-js@^1.0.34: + version "1.0.34" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.34.tgz#b33f41c415325839f354005d25a2f588be296976" + integrity sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew== + +uid@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/uid/-/uid-2.0.1.tgz#a3f57c962828ea65256cd622fc363028cdf4526b" + integrity sha512-PF+1AnZgycpAIEmNtjxGBVmKbZAQguaa4pBUq6KNaGEcpzZ2klCNZLM34tsjp76maN00TttiiUf6zkIBpJQm2A== + dependencies: + "@lukeed/csprng" "^1.0.0" + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz#dbfc5a789caa26b1db8990796c2c8ebbce304824" + integrity sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1, utils-merge@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +v8-to-istanbul@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" + integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + +validator@^13.7.0: + version "13.7.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" + integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vscode-languageserver-textdocument@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz#9eae94509cbd945ea44bca8dcfe4bb0c15bb3ac0" + integrity sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q== + +vscode-uri@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.7.tgz#6d19fef387ee6b46c479e5fb00870e15e58c1eb8" + integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA== + +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +webpack-node-externals@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz#1a3407c158d547a9feb4229a9e3385b7b60c9917" + integrity sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ== + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@5.75.0: + version "5.75.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.75.0.tgz#1e440468647b2505860e94c9ff3e44d5b582c152" + integrity sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.7.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.10.0" + es-module-lexer "^0.9.0" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-typed-array@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" + integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.10" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + +windows-release@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-4.0.0.tgz#4725ec70217d1bf6e02c7772413b29cdde9ec377" + integrity sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg== + dependencies: + execa "^4.0.2" + +winston-daily-rotate-file@^4.7.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/winston-daily-rotate-file/-/winston-daily-rotate-file-4.7.1.tgz#f60a643af87f8867f23170d8cd87dbe3603a625f" + integrity sha512-7LGPiYGBPNyGHLn9z33i96zx/bd71pjBn9tqQzO3I4Tayv94WPmBNwKC7CO1wPHdP9uvu+Md/1nr6VSH9h0iaA== + dependencies: + file-stream-rotator "^0.6.1" + object-hash "^2.0.1" + triple-beam "^1.3.0" + winston-transport "^4.4.0" + +winston-transport@^4.4.0, winston-transport@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa" + integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q== + dependencies: + logform "^2.3.2" + readable-stream "^3.6.0" + triple-beam "^1.3.0" + +winston@^3.8.2: + version "3.8.2" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.8.2.tgz#56e16b34022eb4cff2638196d9646d7430fdad50" + integrity sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew== + dependencies: + "@colors/colors" "1.5.0" + "@dabh/diagnostics" "^2.0.2" + async "^3.2.3" + is-stream "^2.0.0" + logform "^2.4.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + safe-stable-stringify "^2.3.1" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.5.0" + +wmf@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da" + integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw== + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +word@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961" + integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + +xlsx@^0.18.5: + version "0.18.5" + resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.18.5.tgz#16711b9113c848076b8a177022799ad356eba7d0" + integrity sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ== + dependencies: + adler-32 "~1.3.0" + cfb "~1.2.1" + codepage "~1.15.0" + crc-32 "~1.2.1" + ssf "~0.11.2" + wmf "~1.0.1" + word "~0.3.0" + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yargs-parser@21.1.1, yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs-parser@^21.0.0, yargs-parser@^21.0.1: + version "21.0.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" + integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== + +yargs@^17.3.1: + version "17.5.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" + integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.0.0" + +yargs@^17.7.1: + version "17.7.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967" + integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yarn@^1.22.19: + version "1.22.19" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.19.tgz#4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" + integrity sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ== + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==