From cb34dd735026ca529a3278ddfa61cbed97467355 Mon Sep 17 00:00:00 2001 From: Vikalp Date: Thu, 9 Apr 2026 12:17:58 +0530 Subject: [PATCH] Initialize project and add containerization --- .dockerignore | 12 + .env.example | 21 + .gitignore | 5 + API_DOCUMENTATION.md | 73 + Dockerfile | 43 + README.md | 98 + docker-compose.yml | 49 + docs/architecture.md | 51 + errors.txt | Bin 0 -> 430 bytes fix-paths.js | 49 + package-lock.json | 6400 +++++++++++++++++ package.json | 42 + src/app.ts | 52 + src/bootstrap/application-context.ts | 343 + src/main.ts | 41 + .../change-subscription-plan.command.ts | 4 + .../change-subscription-plan.handler.ts | 49 + .../initialize-subscription.command.ts | 3 + .../initialize-subscription.handler.ts | 37 + .../ports/payment-provider.port.ts | 9 + .../get-billing-history.handler.ts | 25 + .../get-billing-history.query.ts | 13 + .../get-tenant-subscription.handler.ts | 33 + .../get-tenant-subscription.query.ts | 13 + .../aggregates/subscription-plan.aggregate.ts | 82 + .../tenant-subscription.aggregate.ts | 80 + .../domain/entities/billing-record.entity.ts | 32 + .../repositories/billing-record.repository.ts | 6 + .../subscription-plan.repository.ts | 8 + .../tenant-subscription.repository.ts | 7 + .../entities/billing-record.orm-entity.ts | 31 + .../entities/subscription-plan.orm-entity.ts | 28 + .../tenant-subscription.orm-entity.ts | 31 + .../typeorm/mappers/billing-record.mapper.ts | 33 + .../mappers/subscription-plan.mapper.ts | 31 + .../mappers/tenant-subscription.mapper.ts | 33 + .../typeorm-billing-record.repository.ts | 28 + .../typeorm-subscription-plan.repository.ts | 40 + .../typeorm-tenant-subscription.repository.ts | 34 + .../infrastructure/services/billing-seeder.ts | 50 + .../services/mock-payment-provider.ts | 23 + .../http/controllers/billing.controller.ts | 52 + .../delete-document.command.ts | 4 + .../delete-document.handler.ts | 25 + .../update-document.command.ts | 11 + .../update-document/update-document.dto.ts | 14 + .../update-document.handler.ts | 50 + .../upload-document.command.ts | 17 + .../upload-document/upload-document.dto.ts | 17 + .../upload-document.handler.ts | 91 + .../mappers/document-input-parser.ts | 93 + .../mappers/document-response.mapper.ts | 54 + .../ports/document-storage.port.ts | 22 + .../download-document.handler.ts | 38 + .../download-document.query.ts | 4 + .../get-document-by-id.handler.ts | 21 + .../get-document-by-id.query.ts | 4 + .../list-documents/list-documents.handler.ts | 17 + .../list-documents/list-documents.query.ts | 8 + .../document-link-validator.service.ts | 87 + .../domain/aggregates/document.aggregate.ts | 206 + .../domain/entities/document-link.entity.ts | 55 + .../repositories/document.repository.ts | 15 + .../entities/document-link.orm-entity.ts | 32 + .../typeorm/entities/document.orm-entity.ts | 60 + .../mappers/document-persistence.mapper.ts | 69 + .../typeorm-document.repository.ts | 87 + .../storage/local-document-storage.service.ts | 54 + .../storage/s3-document-storage.service.ts | 93 + .../http/controllers/documents.controller.ts | 148 + .../assign-roles/assign-roles.command.ts | 5 + .../commands/assign-roles/assign-roles.dto.ts | 3 + .../assign-roles/assign-roles.handler.ts | 42 + .../create-user/create-user.command.ts | 8 + .../commands/create-user/create-user.dto.ts | 12 + .../create-user/create-user.handler.ts | 109 + .../invite-user/invite-user.command.ts | 7 + .../commands/invite-user/invite-user.dto.ts | 13 + .../invite-user/invite-user.handler.ts | 110 + .../commands/login/login.command.ts | 5 + .../application/commands/login/login.dto.ts | 5 + .../commands/login/login.handler.ts | 52 + .../register-user/register-user.command.ts | 7 + .../register-user/register-user.dto.ts | 14 + .../register-user/register-user.handler.ts | 85 + .../update-user-status.command.ts | 7 + .../update-user-status.dto.ts | 5 + .../update-user-status.handler.ts | 40 + .../mappers/user-response.mapper.ts | 29 + .../application/ports/email.port.ts | 9 + .../get-current-user.handler.ts | 21 + .../get-current-user.query.ts | 4 + .../get-user-by-id/get-user-by-id.handler.ts | 21 + .../get-user-by-id/get-user-by-id.query.ts | 4 + .../services/access-token.service.ts | 14 + .../services/auth-principal.factory.ts | 14 + .../domain/aggregates/user.aggregate.ts | 198 + .../domain/entities/permission.entity.ts | 30 + .../domain/entities/role-permission.entity.ts | 17 + .../domain/entities/role.entity.ts | 46 + .../domain/entities/user-role.entity.ts | 18 + .../domain/repositories/role.repository.ts | 5 + .../domain/repositories/user.repository.ts | 8 + .../domain/value-objects/email.vo.ts | 26 + .../domain/value-objects/password-hash.vo.ts | 26 + .../typeorm/entities/permission.orm-entity.ts | 28 + .../entities/role-permission.orm-entity.ts | 33 + .../typeorm/entities/role.orm-entity.ts | 39 + .../typeorm/entities/user-role.orm-entity.ts | 36 + .../typeorm/entities/user.orm-entity.ts | 46 + .../mappers/user-persistence.mapper.ts | 100 + .../repositories/typeorm-role.repository.ts | 44 + .../repositories/typeorm-user.repository.ts | 100 + .../services/console-email-sender.ts | 20 + .../services/identity-access-seeder.ts | 184 + .../http/controllers/auth.controller.ts | 49 + .../http/controllers/users.controller.ts | 93 + .../add-subtask/add-subtask.command.ts | 8 + .../commands/add-subtask/add-subtask.dto.ts | 11 + .../add-subtask/add-subtask.handler.ts | 56 + .../create-task/create-task.command.ts | 10 + .../commands/create-task/create-task.dto.ts | 13 + .../create-task/create-task.handler.ts | 58 + .../delete-subtask/delete-subtask.command.ts | 5 + .../delete-subtask/delete-subtask.handler.ts | 25 + .../delete-task/delete-task.command.ts | 4 + .../delete-task/delete-task.handler.ts | 20 + .../update-subtask/update-subtask.command.ts | 12 + .../update-subtask/update-subtask.dto.ts | 13 + .../update-subtask/update-subtask.handler.ts | 55 + .../update-task/update-task.command.ts | 12 + .../commands/update-task/update-task.dto.ts | 14 + .../update-task/update-task.handler.ts | 55 + .../application/mappers/date-parser.ts | 33 + .../mappers/task-response.mapper.ts | 62 + .../get-task-by-id/get-task-by-id.handler.ts | 21 + .../get-task-by-id/get-task-by-id.query.ts | 4 + .../queries/list-tasks/list-tasks.handler.ts | 16 + .../queries/list-tasks/list-tasks.query.ts | 7 + .../domain/aggregates/task.aggregate.ts | 278 + .../domain/entities/subtask.entity.ts | 120 + .../domain/repositories/task.repository.ts | 17 + .../typeorm/entities/subtask.orm-entity.ts | 46 + .../typeorm/entities/task.orm-entity.ts | 48 + .../mappers/task-persistence.mapper.ts | 82 + .../repositories/typeorm-task.repository.ts | 96 + .../http/controllers/tasks.controller.ts | 139 + .../onboard-tenant/onboard-tenant.command.ts | 5 + .../onboard-tenant/onboard-tenant.dto.ts | 9 + .../onboard-tenant/onboard-tenant.handler.ts | 54 + .../mappers/tenant-response.mapper.ts | 38 + .../ports/tenant-provisioner.port.ts | 3 + .../get-tenant-by-id.handler.ts | 26 + .../get-tenant-by-id.query.ts | 3 + .../resolve-tenant/resolve-tenant.handler.ts | 62 + .../resolve-tenant/resolve-tenant.query.ts | 7 + .../domain/aggregates/tenant.aggregate.ts | 129 + .../tenancy/domain/entities/company.entity.ts | 40 + .../domain/events/tenant-onboarded.event.ts | 13 + .../domain/repositories/tenant.repository.ts | 7 + .../domain/value-objects/tenant-id.vo.ts | 26 + .../typeorm/entities/company.orm-entity.ts | 32 + .../typeorm/entities/tenant.orm-entity.ts | 30 + .../mappers/tenant-persistence.mapper.ts | 49 + .../repositories/typeorm-tenant.repository.ts | 50 + .../http/controllers/tenants.controller.ts | 45 + src/shared/application/common/use-case.ts | 3 + src/shared/application/ports/clock.port.ts | 3 + .../application/ports/id-generator.port.ts | 3 + .../application/ports/password-hasher.port.ts | 4 + src/shared/domain/base/aggregate-root.ts | 17 + src/shared/domain/base/domain-event.ts | 4 + src/shared/domain/base/entity.ts | 3 + src/shared/domain/base/repository.ts | 4 + src/shared/domain/base/value-object.ts | 11 + src/shared/domain/errors/application-error.ts | 10 + src/shared/domain/errors/domain-error.ts | 9 + src/shared/domain/types/request-context.ts | 15 + src/shared/domain/types/tenant-context.ts | 14 + .../infrastructure/config/app-config.ts | 49 + src/shared/infrastructure/config/env.ts | 91 + src/shared/infrastructure/logging/logger.ts | 31 + .../typeorm/data-source.factory.ts | 147 + .../typeorm/tenant-data-source.resolver.ts | 18 + .../security/bcrypt-password-hasher.ts | 15 + .../infrastructure/security/jwt.service.ts | 58 + .../system/node-crypto-id-generator.ts | 9 + .../infrastructure/system/system-clock.ts | 7 + .../http/middlewares/auth.middleware.ts | 30 + .../http/middlewares/authorize.middleware.ts | 53 + .../http/middlewares/error.middleware.ts | 46 + .../http/middlewares/multer.middleware.ts | 8 + .../middlewares/request-context.middleware.ts | 20 + .../middlewares/require-auth.middleware.ts | 17 + .../http/middlewares/tenant.middleware.ts | 69 + .../http/presenters/error.presenter.ts | 40 + .../interfaces/http/tsoa/authentication.ts | 32 + .../interfaces/http/tsoa/generated/routes.ts | 1474 ++++ .../http/tsoa/generated/swagger.json | 1912 +++++ src/shared/interfaces/http/tsoa/ioc.ts | 67 + src/shared/interfaces/http/tsoa/swagger.ts | 48 + src/types/express.d.ts | 14 + tsconfig.json | 21 + tsoa.json | 26 + 204 files changed, 17651 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 API_DOCUMENTATION.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 docs/architecture.md create mode 100644 errors.txt create mode 100644 fix-paths.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/app.ts create mode 100644 src/bootstrap/application-context.ts create mode 100644 src/main.ts create mode 100644 src/modules/billing/application/commands/change-subscription-plan/change-subscription-plan.command.ts create mode 100644 src/modules/billing/application/commands/change-subscription-plan/change-subscription-plan.handler.ts create mode 100644 src/modules/billing/application/commands/initialize-subscription/initialize-subscription.command.ts create mode 100644 src/modules/billing/application/commands/initialize-subscription/initialize-subscription.handler.ts create mode 100644 src/modules/billing/application/ports/payment-provider.port.ts create mode 100644 src/modules/billing/application/queries/get-billing-history/get-billing-history.handler.ts create mode 100644 src/modules/billing/application/queries/get-billing-history/get-billing-history.query.ts create mode 100644 src/modules/billing/application/queries/get-tenant-subscription/get-tenant-subscription.handler.ts create mode 100644 src/modules/billing/application/queries/get-tenant-subscription/get-tenant-subscription.query.ts create mode 100644 src/modules/billing/domain/aggregates/subscription-plan.aggregate.ts create mode 100644 src/modules/billing/domain/aggregates/tenant-subscription.aggregate.ts create mode 100644 src/modules/billing/domain/entities/billing-record.entity.ts create mode 100644 src/modules/billing/domain/repositories/billing-record.repository.ts create mode 100644 src/modules/billing/domain/repositories/subscription-plan.repository.ts create mode 100644 src/modules/billing/domain/repositories/tenant-subscription.repository.ts create mode 100644 src/modules/billing/infrastructure/persistence/typeorm/entities/billing-record.orm-entity.ts create mode 100644 src/modules/billing/infrastructure/persistence/typeorm/entities/subscription-plan.orm-entity.ts create mode 100644 src/modules/billing/infrastructure/persistence/typeorm/entities/tenant-subscription.orm-entity.ts create mode 100644 src/modules/billing/infrastructure/persistence/typeorm/mappers/billing-record.mapper.ts create mode 100644 src/modules/billing/infrastructure/persistence/typeorm/mappers/subscription-plan.mapper.ts create mode 100644 src/modules/billing/infrastructure/persistence/typeorm/mappers/tenant-subscription.mapper.ts create mode 100644 src/modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-billing-record.repository.ts create mode 100644 src/modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-subscription-plan.repository.ts create mode 100644 src/modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-tenant-subscription.repository.ts create mode 100644 src/modules/billing/infrastructure/services/billing-seeder.ts create mode 100644 src/modules/billing/infrastructure/services/mock-payment-provider.ts create mode 100644 src/modules/billing/interfaces/http/controllers/billing.controller.ts create mode 100644 src/modules/document-management/application/commands/delete-document/delete-document.command.ts create mode 100644 src/modules/document-management/application/commands/delete-document/delete-document.handler.ts create mode 100644 src/modules/document-management/application/commands/update-document/update-document.command.ts create mode 100644 src/modules/document-management/application/commands/update-document/update-document.dto.ts create mode 100644 src/modules/document-management/application/commands/update-document/update-document.handler.ts create mode 100644 src/modules/document-management/application/commands/upload-document/upload-document.command.ts create mode 100644 src/modules/document-management/application/commands/upload-document/upload-document.dto.ts create mode 100644 src/modules/document-management/application/commands/upload-document/upload-document.handler.ts create mode 100644 src/modules/document-management/application/mappers/document-input-parser.ts create mode 100644 src/modules/document-management/application/mappers/document-response.mapper.ts create mode 100644 src/modules/document-management/application/ports/document-storage.port.ts create mode 100644 src/modules/document-management/application/queries/download-document/download-document.handler.ts create mode 100644 src/modules/document-management/application/queries/download-document/download-document.query.ts create mode 100644 src/modules/document-management/application/queries/get-document-by-id/get-document-by-id.handler.ts create mode 100644 src/modules/document-management/application/queries/get-document-by-id/get-document-by-id.query.ts create mode 100644 src/modules/document-management/application/queries/list-documents/list-documents.handler.ts create mode 100644 src/modules/document-management/application/queries/list-documents/list-documents.query.ts create mode 100644 src/modules/document-management/application/services/document-link-validator.service.ts create mode 100644 src/modules/document-management/domain/aggregates/document.aggregate.ts create mode 100644 src/modules/document-management/domain/entities/document-link.entity.ts create mode 100644 src/modules/document-management/domain/repositories/document.repository.ts create mode 100644 src/modules/document-management/infrastructure/persistence/typeorm/entities/document-link.orm-entity.ts create mode 100644 src/modules/document-management/infrastructure/persistence/typeorm/entities/document.orm-entity.ts create mode 100644 src/modules/document-management/infrastructure/persistence/typeorm/mappers/document-persistence.mapper.ts create mode 100644 src/modules/document-management/infrastructure/persistence/typeorm/repositories/typeorm-document.repository.ts create mode 100644 src/modules/document-management/infrastructure/storage/local-document-storage.service.ts create mode 100644 src/modules/document-management/infrastructure/storage/s3-document-storage.service.ts create mode 100644 src/modules/document-management/interfaces/http/controllers/documents.controller.ts create mode 100644 src/modules/identity-access/application/commands/assign-roles/assign-roles.command.ts create mode 100644 src/modules/identity-access/application/commands/assign-roles/assign-roles.dto.ts create mode 100644 src/modules/identity-access/application/commands/assign-roles/assign-roles.handler.ts create mode 100644 src/modules/identity-access/application/commands/create-user/create-user.command.ts create mode 100644 src/modules/identity-access/application/commands/create-user/create-user.dto.ts create mode 100644 src/modules/identity-access/application/commands/create-user/create-user.handler.ts create mode 100644 src/modules/identity-access/application/commands/invite-user/invite-user.command.ts create mode 100644 src/modules/identity-access/application/commands/invite-user/invite-user.dto.ts create mode 100644 src/modules/identity-access/application/commands/invite-user/invite-user.handler.ts create mode 100644 src/modules/identity-access/application/commands/login/login.command.ts create mode 100644 src/modules/identity-access/application/commands/login/login.dto.ts create mode 100644 src/modules/identity-access/application/commands/login/login.handler.ts create mode 100644 src/modules/identity-access/application/commands/register-user/register-user.command.ts create mode 100644 src/modules/identity-access/application/commands/register-user/register-user.dto.ts create mode 100644 src/modules/identity-access/application/commands/register-user/register-user.handler.ts create mode 100644 src/modules/identity-access/application/commands/update-user-status/update-user-status.command.ts create mode 100644 src/modules/identity-access/application/commands/update-user-status/update-user-status.dto.ts create mode 100644 src/modules/identity-access/application/commands/update-user-status/update-user-status.handler.ts create mode 100644 src/modules/identity-access/application/mappers/user-response.mapper.ts create mode 100644 src/modules/identity-access/application/ports/email.port.ts create mode 100644 src/modules/identity-access/application/queries/get-current-user/get-current-user.handler.ts create mode 100644 src/modules/identity-access/application/queries/get-current-user/get-current-user.query.ts create mode 100644 src/modules/identity-access/application/queries/get-user-by-id/get-user-by-id.handler.ts create mode 100644 src/modules/identity-access/application/queries/get-user-by-id/get-user-by-id.query.ts create mode 100644 src/modules/identity-access/application/services/access-token.service.ts create mode 100644 src/modules/identity-access/application/services/auth-principal.factory.ts create mode 100644 src/modules/identity-access/domain/aggregates/user.aggregate.ts create mode 100644 src/modules/identity-access/domain/entities/permission.entity.ts create mode 100644 src/modules/identity-access/domain/entities/role-permission.entity.ts create mode 100644 src/modules/identity-access/domain/entities/role.entity.ts create mode 100644 src/modules/identity-access/domain/entities/user-role.entity.ts create mode 100644 src/modules/identity-access/domain/repositories/role.repository.ts create mode 100644 src/modules/identity-access/domain/repositories/user.repository.ts create mode 100644 src/modules/identity-access/domain/value-objects/email.vo.ts create mode 100644 src/modules/identity-access/domain/value-objects/password-hash.vo.ts create mode 100644 src/modules/identity-access/infrastructure/persistence/typeorm/entities/permission.orm-entity.ts create mode 100644 src/modules/identity-access/infrastructure/persistence/typeorm/entities/role-permission.orm-entity.ts create mode 100644 src/modules/identity-access/infrastructure/persistence/typeorm/entities/role.orm-entity.ts create mode 100644 src/modules/identity-access/infrastructure/persistence/typeorm/entities/user-role.orm-entity.ts create mode 100644 src/modules/identity-access/infrastructure/persistence/typeorm/entities/user.orm-entity.ts create mode 100644 src/modules/identity-access/infrastructure/persistence/typeorm/mappers/user-persistence.mapper.ts create mode 100644 src/modules/identity-access/infrastructure/persistence/typeorm/repositories/typeorm-role.repository.ts create mode 100644 src/modules/identity-access/infrastructure/persistence/typeorm/repositories/typeorm-user.repository.ts create mode 100644 src/modules/identity-access/infrastructure/services/console-email-sender.ts create mode 100644 src/modules/identity-access/infrastructure/services/identity-access-seeder.ts create mode 100644 src/modules/identity-access/interfaces/http/controllers/auth.controller.ts create mode 100644 src/modules/identity-access/interfaces/http/controllers/users.controller.ts create mode 100644 src/modules/task-management/application/commands/add-subtask/add-subtask.command.ts create mode 100644 src/modules/task-management/application/commands/add-subtask/add-subtask.dto.ts create mode 100644 src/modules/task-management/application/commands/add-subtask/add-subtask.handler.ts create mode 100644 src/modules/task-management/application/commands/create-task/create-task.command.ts create mode 100644 src/modules/task-management/application/commands/create-task/create-task.dto.ts create mode 100644 src/modules/task-management/application/commands/create-task/create-task.handler.ts create mode 100644 src/modules/task-management/application/commands/delete-subtask/delete-subtask.command.ts create mode 100644 src/modules/task-management/application/commands/delete-subtask/delete-subtask.handler.ts create mode 100644 src/modules/task-management/application/commands/delete-task/delete-task.command.ts create mode 100644 src/modules/task-management/application/commands/delete-task/delete-task.handler.ts create mode 100644 src/modules/task-management/application/commands/update-subtask/update-subtask.command.ts create mode 100644 src/modules/task-management/application/commands/update-subtask/update-subtask.dto.ts create mode 100644 src/modules/task-management/application/commands/update-subtask/update-subtask.handler.ts create mode 100644 src/modules/task-management/application/commands/update-task/update-task.command.ts create mode 100644 src/modules/task-management/application/commands/update-task/update-task.dto.ts create mode 100644 src/modules/task-management/application/commands/update-task/update-task.handler.ts create mode 100644 src/modules/task-management/application/mappers/date-parser.ts create mode 100644 src/modules/task-management/application/mappers/task-response.mapper.ts create mode 100644 src/modules/task-management/application/queries/get-task-by-id/get-task-by-id.handler.ts create mode 100644 src/modules/task-management/application/queries/get-task-by-id/get-task-by-id.query.ts create mode 100644 src/modules/task-management/application/queries/list-tasks/list-tasks.handler.ts create mode 100644 src/modules/task-management/application/queries/list-tasks/list-tasks.query.ts create mode 100644 src/modules/task-management/domain/aggregates/task.aggregate.ts create mode 100644 src/modules/task-management/domain/entities/subtask.entity.ts create mode 100644 src/modules/task-management/domain/repositories/task.repository.ts create mode 100644 src/modules/task-management/infrastructure/persistence/typeorm/entities/subtask.orm-entity.ts create mode 100644 src/modules/task-management/infrastructure/persistence/typeorm/entities/task.orm-entity.ts create mode 100644 src/modules/task-management/infrastructure/persistence/typeorm/mappers/task-persistence.mapper.ts create mode 100644 src/modules/task-management/infrastructure/persistence/typeorm/repositories/typeorm-task.repository.ts create mode 100644 src/modules/task-management/interfaces/http/controllers/tasks.controller.ts create mode 100644 src/modules/tenancy/application/commands/onboard-tenant/onboard-tenant.command.ts create mode 100644 src/modules/tenancy/application/commands/onboard-tenant/onboard-tenant.dto.ts create mode 100644 src/modules/tenancy/application/commands/onboard-tenant/onboard-tenant.handler.ts create mode 100644 src/modules/tenancy/application/mappers/tenant-response.mapper.ts create mode 100644 src/modules/tenancy/application/ports/tenant-provisioner.port.ts create mode 100644 src/modules/tenancy/application/queries/get-tenant-by-id/get-tenant-by-id.handler.ts create mode 100644 src/modules/tenancy/application/queries/get-tenant-by-id/get-tenant-by-id.query.ts create mode 100644 src/modules/tenancy/application/queries/resolve-tenant/resolve-tenant.handler.ts create mode 100644 src/modules/tenancy/application/queries/resolve-tenant/resolve-tenant.query.ts create mode 100644 src/modules/tenancy/domain/aggregates/tenant.aggregate.ts create mode 100644 src/modules/tenancy/domain/entities/company.entity.ts create mode 100644 src/modules/tenancy/domain/events/tenant-onboarded.event.ts create mode 100644 src/modules/tenancy/domain/repositories/tenant.repository.ts create mode 100644 src/modules/tenancy/domain/value-objects/tenant-id.vo.ts create mode 100644 src/modules/tenancy/infrastructure/persistence/typeorm/entities/company.orm-entity.ts create mode 100644 src/modules/tenancy/infrastructure/persistence/typeorm/entities/tenant.orm-entity.ts create mode 100644 src/modules/tenancy/infrastructure/persistence/typeorm/mappers/tenant-persistence.mapper.ts create mode 100644 src/modules/tenancy/infrastructure/persistence/typeorm/repositories/typeorm-tenant.repository.ts create mode 100644 src/modules/tenancy/interfaces/http/controllers/tenants.controller.ts create mode 100644 src/shared/application/common/use-case.ts create mode 100644 src/shared/application/ports/clock.port.ts create mode 100644 src/shared/application/ports/id-generator.port.ts create mode 100644 src/shared/application/ports/password-hasher.port.ts create mode 100644 src/shared/domain/base/aggregate-root.ts create mode 100644 src/shared/domain/base/domain-event.ts create mode 100644 src/shared/domain/base/entity.ts create mode 100644 src/shared/domain/base/repository.ts create mode 100644 src/shared/domain/base/value-object.ts create mode 100644 src/shared/domain/errors/application-error.ts create mode 100644 src/shared/domain/errors/domain-error.ts create mode 100644 src/shared/domain/types/request-context.ts create mode 100644 src/shared/domain/types/tenant-context.ts create mode 100644 src/shared/infrastructure/config/app-config.ts create mode 100644 src/shared/infrastructure/config/env.ts create mode 100644 src/shared/infrastructure/logging/logger.ts create mode 100644 src/shared/infrastructure/persistence/typeorm/data-source.factory.ts create mode 100644 src/shared/infrastructure/persistence/typeorm/tenant-data-source.resolver.ts create mode 100644 src/shared/infrastructure/security/bcrypt-password-hasher.ts create mode 100644 src/shared/infrastructure/security/jwt.service.ts create mode 100644 src/shared/infrastructure/system/node-crypto-id-generator.ts create mode 100644 src/shared/infrastructure/system/system-clock.ts create mode 100644 src/shared/interfaces/http/middlewares/auth.middleware.ts create mode 100644 src/shared/interfaces/http/middlewares/authorize.middleware.ts create mode 100644 src/shared/interfaces/http/middlewares/error.middleware.ts create mode 100644 src/shared/interfaces/http/middlewares/multer.middleware.ts create mode 100644 src/shared/interfaces/http/middlewares/request-context.middleware.ts create mode 100644 src/shared/interfaces/http/middlewares/require-auth.middleware.ts create mode 100644 src/shared/interfaces/http/middlewares/tenant.middleware.ts create mode 100644 src/shared/interfaces/http/presenters/error.presenter.ts create mode 100644 src/shared/interfaces/http/tsoa/authentication.ts create mode 100644 src/shared/interfaces/http/tsoa/generated/routes.ts create mode 100644 src/shared/interfaces/http/tsoa/generated/swagger.json create mode 100644 src/shared/interfaces/http/tsoa/ioc.ts create mode 100644 src/shared/interfaces/http/tsoa/swagger.ts create mode 100644 src/types/express.d.ts create mode 100644 tsconfig.json create mode 100644 tsoa.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c11cdbd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +dist +npm-debug.log +.env +.git +.gitignore +Dockerfile +docker-compose.yml +.dockerignore +README.md +docs +errors.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c2434e8 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +NODE_ENV=development +PORT=3000 +API_BASE_PATH=/api +SWAGGER_PATH=/docs +JWT_SECRET=change-me +JWT_EXPIRES_IN=1h +JWT_ISSUER=gem-track-backend +JWT_AUDIENCE=gem-track-api +BCRYPT_ROUNDS=12 + +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=gem_track +DB_USER=postgres +DB_PASSWORD=postgres +DB_SCHEMA=public +DB_SSL=false +TYPEORM_SYNCHRONIZE=false +TYPEORM_LOGGING=false + +TENANCY_MODE=shared-schema diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..184aba2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +.env.local +coverage/ diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..d732d96 --- /dev/null +++ b/API_DOCUMENTATION.md @@ -0,0 +1,73 @@ +# Gem Track Backend - API Documentation Overview + +Welcome to the backend API reference for **Gem Track**! This guide is designed to help frontend developers and API integrators understand the available resources, authentication flow, and how to start making requests locally. + +## Detailed OpenAPI Reference + +The backend uses **TSOA**, which means that our API routes and schemas are strongly-typed and automatically generated. + +You can access the full Interactive Swagger API documentation by navigating to: +👉 **[http://localhost:3000/docs](http://localhost:3000/docs)** + +*(Make sure you have started your Docker environment via `docker compose up` first)* + +From the Swagger UI, you can: +- **Test Endpoints**: Execute API requests directly from your browser. +- **View Schemas**: Inspect the exact JSON payload shapes for requests and responses. +- **Download Specification**: Grab the raw `swagger.json` file to auto-generate frontend SDKs/clients using generic tools (like OpenAPI Generator or Orval). + +--- + +## High-Level Capabilities + +The API exposes endpoints divided into distinct domains: + +### 1. Tenancy (`/api/v1/tenants`) +Gem Track is a multi-tenant application. Every organization runs in its own tenant container. +- **`POST /api/v1/tenants`**: Onboard a new tenant (this sets up default DB rows, billing plans, roles, and permissions). +- **`GET /api/v1/tenants/{tenantId}`**: Fetch details about an active tenant. + +### 2. Authentication (`/api/v1/auth`) +Tokens are issued via JWT and must be provided natively in the `Authorization` header (`Bearer `). +- **`POST /api/v1/auth/register`**: Enroll the first administrator user for a new tenant. +- **`POST /api/v1/auth/login`**: Acquire a JWT credential by authenticating. +- **`GET /api/v1/auth/me`**: Returns profile info, active roles, and authorization scopes (permissions) for the current user. + +### 3. User Management (`/api/v1/users`) +- **`POST /api/v1/users`**: Directly create a new user profile inside a tenant. +- **`POST /api/v1/users/invite`**: Issue an invitation to a new team member. +- **`PATCH /api/v1/users/{userId}/roles`**: Assign operational security roles (`tenant_admin` or `tenant_user`). +- **`PATCH /api/v1/users/{userId}/status`**: Adjust user standing (active/suspended). +- **`GET /api/v1/users/{userId}`**: Extract scoped profile details. + +### 4. Billing (`/api/v1/billing`) +Tracks the assigned tier usage context logic. +- **`GET /api/v1/billing/subscription`**: Pull the current subscription parameters (e.g., Free vs Pro vs Enterprise limits). +- **`POST /api/v1/billing/subscription/plan`**: Downgrade/Upgrade the overarching plan context constraints. +- **`GET /api/v1/billing/history`**: Return paginated historical statement data. + +### 5. Task Management (`/api/v1/tasks`) +Agile tracking mechanics for users operating within the ecosystem. +- **`POST /api/v1/tasks`**: Map a new task context. +- **`GET /api/v1/tasks`**: Read parameters, scope visibility based on user tenant constraints. +- **`PATCH /api/v1/tasks/{taskId}` & Sub-Tasks**: Mutate progress milestones spanning assignments. + +### 6. Document Management (`/api/v1/documents`) +- **`POST /api/v1/documents`**: Implements multi-part form data uploads for handling physical file ingest scaling. +- **`GET /api/v1/documents`**: Retrieves associated files stored alongside records. + +--- + +## Common Workflows + +### System Setup +1. **Create the Tenant**: Hit `POST /api/v1/tenants` +2. **Create the Admin User**: Hit `POST /api/v1/auth/register` using the UUID returned from step 1. +3. **Login in App**: Send credentials via `POST /api/v1/auth/login` and persist the generated `accessToken`. + +### Authenticating Requests +Once you have the `accessToken` from login, all requests tagged with the lock icon in Swagger (which require the `bearerAuth` security schema) must include the following header: + +```http +Authorization: Bearer +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5a01db9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +# Stage 1: Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install all dependencies (including devDependencies) +RUN npm install + +# Copy source code and config files +COPY . . + +# Generate TSOA specs/routes and build the project +RUN npm run build + +# Stage 2: Production stage +FROM node:20-alpine AS runner + +WORKDIR /app + +# Set production environment +ENV NODE_ENV=production + +# Copy package files +COPY package*.json ./ + +# Install only production dependencies +RUN npm ci --only=production + +# Copy built files from the builder stage +COPY --from=builder /app/dist ./dist +# If TSOA requires the generated routes to be in src/shared, copy them as well +# Copy the entire src directory if needed for TSOA routes or other assets +# Actually, dist/main.js should have everything if bundled, but TSOA routes are usually required at runtime +COPY --from=builder /app/src/shared/interfaces/http/tsoa/generated ./src/shared/interfaces/http/tsoa/generated + +# Expose the application port +EXPOSE 3000 + +# Start the application +CMD ["npm", "start"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..855b641 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# Gem Track Backend Starter + +This repository is scaffolded as a production-ready modular monolith using: + +- Node.js +- TypeScript +- Express +- TypeORM +- TSOA +- Swagger UI + +## Architecture Goals + +- Clean Architecture + Domain Driven Design +- Strict separation of `domain`, `application`, `infrastructure`, and `interfaces` +- Multi-tenant SaaS readiness +- Scalability through bounded contexts +- Swagger/OpenAPI documentation via TSOA + +## Folder Structure + +```text +src/ + bootstrap/ # Composition root and dependency wiring + main.ts # Process startup and graceful shutdown + app.ts # Express application assembly + + shared/ + domain/ # Pure business abstractions + application/ # Cross-cutting use-case ports/contracts + infrastructure/ # DB, security, logging, config, messaging + interfaces/http/ # Middleware, presenters, TSOA, Swagger + + modules/ + tenancy/ + domain/ # Aggregate, repository contract, events + application/ # Commands, queries, DTOs, handlers + infrastructure/ # TypeORM entity, mapper, repository impl + interfaces/http/controllers/ # TSOA controllers + + types/ # Framework type augmentation +``` + +## Dependency Flow + +Dependencies always point inward: + +`interfaces -> application -> domain` + +`infrastructure` implements ports defined by `application` or contracts defined by `domain`. + +Rules: + +- `domain` does not import Express, TSOA, TypeORM, or database code. +- `application` orchestrates use cases and depends only on domain contracts and abstract ports. +- `interfaces` is transport-only and should never hold business rules. +- `repositories` return domain aggregates, not ORM entities. +- `TypeORM entities` stay in infrastructure and are mapped to/from domain aggregates. + +## Multi-Tenant Strategy + +The scaffold is built around a tenant-aware request context: + +- `tenant.middleware` resolves tenant from headers, subdomain, or request metadata. +- `TenantContext` flows from the HTTP edge into application and infrastructure. +- `tenant-data-source.resolver.ts` centralizes tenancy-aware database access. +- Shared-schema is the default mode, but the resolver is intentionally shaped so you can grow into schema-per-tenant or database-per-tenant. + +## Swagger + +Swagger is exposed from: + +- `GET /docs` +- `GET /swagger.json` + +TSOA controller decorators are used for OpenAPI generation, and the generated artifacts are written to: + +- `src/shared/interfaces/http/tsoa/generated/routes.ts` +- `src/shared/interfaces/http/tsoa/generated/swagger.json` + +The repository also includes starter generated files so the scaffold is understandable before running code generation. + +## Sample Module + +The included `tenancy` bounded context demonstrates: + +- Domain aggregate with business invariants +- Application command/query handlers +- Repository contract in the domain layer +- TypeORM repository implementation in infrastructure +- TSOA controller in interfaces + +## Suggested Next Steps + +1. Install dependencies with `npm install`. +2. Generate TSOA artifacts with `npm run tsoa:spec && npm run tsoa:routes`. +3. Extend `modules/` with bounded contexts such as `identity-access`, `catalog`, `billing`, and `orders`. +4. Add migrations, outbox processing, authentication policies, and integration tests. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3eb5ebc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,49 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "${PORT:-3000}:3000" + environment: + - NODE_ENV=production + - DB_HOST=db + - DB_PORT=5432 + - DB_NAME=${DB_NAME:-gem_track} + - DB_USER=${DB_USER:-postgres} + - DB_PASSWORD=${DB_PASSWORD:-postgres} + - JWT_SECRET=${JWT_SECRET:-change-me-in-production} + - API_BASE_PATH=${API_BASE_PATH:-/api} + - SWAGGER_PATH=${SWAGGER_PATH:-/docs} + - TYPEORM_SYNCHRONIZE=true + - DB_INITIALIZE_ON_BOOTSTRAP=true + depends_on: + db: + condition: service_healthy + networks: + - gem-track-network + + db: + image: postgres:16-alpine + environment: + - POSTGRES_DB=${DB_NAME:-gem_track} + - POSTGRES_USER=${DB_USER:-postgres} + - POSTGRES_PASSWORD=${DB_PASSWORD:-postgres} + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-gem_track}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - gem-track-network + +networks: + gem-track-network: + driver: bridge + +volumes: + pgdata: diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..f4936e4 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,51 @@ +# Backend Architecture + +## Layer Responsibilities + +### Domain + +- Encapsulates business rules and invariants. +- Contains aggregates, entities, value objects, domain events, and repository interfaces. +- Has no knowledge of HTTP, databases, or frameworks. + +### Application + +- Implements use cases through command and query handlers. +- Coordinates domain models, authorization, transactional boundaries, and DTO shaping. +- Depends on domain contracts and abstract infrastructure ports. + +### Infrastructure + +- Implements technical details such as TypeORM repositories, database connections, JWT, logging, caching, messaging, and external services. +- Maps ORM entities to domain aggregates. +- Can be swapped without changing use-case or domain logic. + +### Interfaces + +- Adapts delivery mechanisms such as HTTP, queues, or scheduled jobs to application use cases. +- TSOA controllers parse requests and delegate to application handlers. +- Middleware resolves request-scoped concerns like tenant context, authentication, tracing, and error formatting. + +## Interaction Flow + +1. HTTP request reaches Express. +2. Middleware enriches the request with `RequestContext` and `TenantContext`. +3. TSOA controller transforms the request into a command/query DTO. +4. Application handler invokes domain behavior using repository contracts. +5. Infrastructure repository loads/persists aggregates through TypeORM entities and mappers. +6. Controller returns a response DTO. +7. Error middleware converts exceptions into consistent API responses. + +## Multi-Tenant Notes + +The recommended progression is: + +1. Shared database / shared schema with `tenant_id` enforcement in repositories. +2. Schema-per-tenant for higher isolation. +3. Database-per-tenant for top-tier compliance or noisy-neighbor isolation. + +Keep tenancy decisions at the edge and in infrastructure: + +- Controllers should not manually enforce tenant filters. +- Application handlers should accept tenant context only when the use case is tenant-specific. +- Infrastructure repositories must apply tenant scope consistently. diff --git a/errors.txt b/errors.txt new file mode 100644 index 0000000000000000000000000000000000000000..81e9fbe1e0bcc639ccfa56004f0f63aa955711a8 GIT binary patch literal 430 zcmZ{g%?iRm420(__zrthL9Krvc=ZwV1xj1B&_BBM;LEFDwulPK!jefQlT03WuU3g7 zm8zBMszRAOI0Yx*5?ra%j3_3Gbtd!5M&O25D2E-x%jv6vUSG-3|E-t9YnWi#4ZX%Q z_MD8@SW8W{)% zMsM%m9Gh13G2OiU_B{JwjhNo=ng=(I>lkj~c--2J(x82Y+37|uBTD%bu4&hJU(>Vm HPy;;y<4aAk literal 0 HcmV?d00001 diff --git a/fix-paths.js b/fix-paths.js new file mode 100644 index 0000000..b2a6752 --- /dev/null +++ b/fix-paths.js @@ -0,0 +1,49 @@ +const fs = require('fs'); +const path = require('path'); + +const srcDir = path.resolve(__dirname, 'src'); +const applicationErrorPath = path.resolve(srcDir, 'shared/domain/errors/application-error'); + +function walkDir(dir, callback) { + fs.readdirSync(dir).forEach(file => { + const fullPath = path.join(dir, file); + if (fs.statSync(fullPath).isDirectory()) { + walkDir(fullPath, callback); + } else if (fullPath.endsWith('.ts')) { + callback(fullPath); + } + }); +} + +walkDir(path.resolve(srcDir, 'modules'), file => { + let content = fs.readFileSync(file, 'utf8'); + // Match any import containing application-error + const regex = /from\s+['"]([^'"]*application-error)['"]/g; + let matches = [...content.matchAll(regex)]; + + if (matches.length > 0) { + let changed = false; + for (const match of matches) { + const importPath = match[1]; + // calculate correct relative path + const dirOfFile = path.dirname(file); + // Let's use posix for imports + let correctRelative = path.posix.relative( + dirOfFile.split(path.sep).join(path.posix.sep), + applicationErrorPath.split(path.sep).join(path.posix.sep) + ); + if (!correctRelative.startsWith('.')) { + correctRelative = './' + correctRelative; + } + + if (importPath !== correctRelative) { + console.log(`Fixing ${path.relative(srcDir, file)}: ${importPath} -> ${correctRelative}`); + content = content.replace(match[0], `from "${correctRelative}"`); + changed = true; + } + } + if (changed) { + fs.writeFileSync(file, content); + } + } +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8481fe2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6400 @@ +{ + "name": "gem-track-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gem-track-backend", + "version": "1.0.0", + "dependencies": { + "@aws-sdk/client-s3": "^3.1026.0", + "@aws-sdk/lib-storage": "^3.1026.0", + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", + "multer": "^2.1.1", + "pg": "^8.20.0", + "reflect-metadata": "^0.2.2", + "swagger-ui-express": "^5.0.1", + "tsoa": "^6.6.0", + "typeorm": "^0.3.24" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.1", + "@types/jsonwebtoken": "^9.0.9", + "@types/multer": "^2.1.0", + "@types/node": "^22.13.10", + "@types/swagger-ui-express": "^4.1.8", + "ts-node": "^10.9.2", + "tsx": "^4.19.3", + "typescript": "^5.8.2" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1026.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1026.0.tgz", + "integrity": "sha512-tMP+s641FLSXdJazvYvuf38F7suWWv+wagTvShykPTffuFpBj5J9f7Rw0eKsauBcsjPSntiwBz9Gm0Tlh+cKfQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/credential-provider-node": "^3.972.30", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.9", + "@aws-sdk/middleware-expect-continue": "^3.972.9", + "@aws-sdk/middleware-flexible-checksums": "^3.974.7", + "@aws-sdk/middleware-host-header": "^3.972.9", + "@aws-sdk/middleware-location-constraint": "^3.972.9", + "@aws-sdk/middleware-logger": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.10", + "@aws-sdk/middleware-sdk-s3": "^3.972.28", + "@aws-sdk/middleware-ssec": "^3.972.9", + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/region-config-resolver": "^3.972.11", + "@aws-sdk/signature-v4-multi-region": "^3.996.16", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@aws-sdk/util-user-agent-browser": "^3.972.9", + "@aws-sdk/util-user-agent-node": "^3.973.15", + "@smithy/config-resolver": "^4.4.14", + "@smithy/core": "^3.23.14", + "@smithy/eventstream-serde-browser": "^4.2.13", + "@smithy/eventstream-serde-config-resolver": "^4.3.13", + "@smithy/eventstream-serde-node": "^4.2.13", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/hash-blob-browser": "^4.2.14", + "@smithy/hash-node": "^4.2.13", + "@smithy/hash-stream-node": "^4.2.13", + "@smithy/invalid-dependency": "^4.2.13", + "@smithy/md5-js": "^4.2.13", + "@smithy/middleware-content-length": "^4.2.13", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-retry": "^4.5.0", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.45", + "@smithy/util-defaults-mode-node": "^4.2.49", + "@smithy/util-endpoints": "^3.3.4", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.0", + "@smithy/util-stream": "^4.5.22", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.15", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.973.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.27.tgz", + "integrity": "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/xml-builder": "^3.972.17", + "@smithy/core": "^3.23.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/signature-v4": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.6.tgz", + "integrity": "sha512-NMbiqKdruhwwgI6nzBVe2jWMkXjaoQz2YOs3rFX+2F3gGyrJDkDPwMpV/RsTFeq2vAQ055wZNtOXFK4NYSkM8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.25.tgz", + "integrity": "sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.27.tgz", + "integrity": "sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-stream": "^4.5.22", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.29.tgz", + "integrity": "sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/credential-provider-env": "^3.972.25", + "@aws-sdk/credential-provider-http": "^3.972.27", + "@aws-sdk/credential-provider-login": "^3.972.29", + "@aws-sdk/credential-provider-process": "^3.972.25", + "@aws-sdk/credential-provider-sso": "^3.972.29", + "@aws-sdk/credential-provider-web-identity": "^3.972.29", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.29.tgz", + "integrity": "sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.30.tgz", + "integrity": "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.25", + "@aws-sdk/credential-provider-http": "^3.972.27", + "@aws-sdk/credential-provider-ini": "^3.972.29", + "@aws-sdk/credential-provider-process": "^3.972.25", + "@aws-sdk/credential-provider-sso": "^3.972.29", + "@aws-sdk/credential-provider-web-identity": "^3.972.29", + "@aws-sdk/types": "^3.973.7", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.25.tgz", + "integrity": "sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.29.tgz", + "integrity": "sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/token-providers": "3.1026.0", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.29.tgz", + "integrity": "sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/lib-storage": { + "version": "3.1026.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1026.0.tgz", + "integrity": "sha512-zxNkf7PyQ7XOJeWYBcHCAQGcNy0knA2wdNQtgZ13TtPtSAaKhpqSz0eIc55j05f91YoFPKXKkuYhqNHUCn8nGg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "buffer": "5.6.0", + "events": "3.3.0", + "stream-browserify": "3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.1026.0" + } + }, + "node_modules/@aws-sdk/lib-storage/node_modules/buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.9.tgz", + "integrity": "sha512-COToYKgquDyligbcAep7ygs48RK+mwe/IYprq4+TSrVFzNOYmzWvHf6werpnKV5VYpRiwdn+Wa5ZXkPqLVwcTg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.9.tgz", + "integrity": "sha512-V/FNCjFxnh4VGu+HdSiW4Yg5GELihA1MIDSAdsEPvuayXBVmr0Jaa6jdLAZLH38KYXl/vVjri9DQJWnTAujHEA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.974.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.7.tgz", + "integrity": "sha512-uU4/ch2CLHB8Phu1oTKnnQ4e8Ujqi49zEnQYBhWYT53zfFvtJCdGsaOoypBr8Fm/pmCBssRmGoIQ4sixgdLP9w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/crc64-nvme": "^3.972.6", + "@aws-sdk/types": "^3.973.7", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-stream": "^4.5.22", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.9.tgz", + "integrity": "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.9.tgz", + "integrity": "sha512-TyfOi2XNdOZpNKeTJwRUsVAGa+14nkyMb2VVGG+eDgcWG/ed6+NUo72N3hT6QJioxym80NSinErD+LBRF0Ir1w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.9.tgz", + "integrity": "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.10.tgz", + "integrity": "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.28", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.28.tgz", + "integrity": "sha512-qJHcJQH9UNPUrnPlRtCozKjtqAaypQ5IgQxTNoPsVYIQeuwNIA8Rwt3NvGij1vCDYDfCmZaPLpnJEHlZXeFqmg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/signature-v4": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-stream": "^4.5.22", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.9.tgz", + "integrity": "sha512-wSA2BR7L0CyBNDJeSrleIIzC+DzL93YNTdfU0KPGLiocK6YsRv1nPAzPF+BFSdcs0Qa5ku5Kcf4KvQcWwKGenQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.29.tgz", + "integrity": "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@smithy/core": "^3.23.14", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-retry": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.996.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.19.tgz", + "integrity": "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/middleware-host-header": "^3.972.9", + "@aws-sdk/middleware-logger": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.10", + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/region-config-resolver": "^3.972.11", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@aws-sdk/util-user-agent-browser": "^3.972.9", + "@aws-sdk/util-user-agent-node": "^3.973.15", + "@smithy/config-resolver": "^4.4.14", + "@smithy/core": "^3.23.14", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/hash-node": "^4.2.13", + "@smithy/invalid-dependency": "^4.2.13", + "@smithy/middleware-content-length": "^4.2.13", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-retry": "^4.5.0", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.45", + "@smithy/util-defaults-mode-node": "^4.2.49", + "@smithy/util-endpoints": "^3.3.4", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.0", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.11.tgz", + "integrity": "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/config-resolver": "^4.4.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.16.tgz", + "integrity": "sha512-EMdXYB4r/k5RWq86fugjRhid5JA+Z6MpS7n4sij4u5/C+STrkvuf9aFu41rJA9MjUzxCLzv8U2XL8cH2GSRYpQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.28", + "@aws-sdk/types": "^3.973.7", + "@smithy/protocol-http": "^5.3.13", + "@smithy/signature-v4": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1026.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1026.0.tgz", + "integrity": "sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.7.tgz", + "integrity": "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.6.tgz", + "integrity": "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-endpoints": "^3.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.9.tgz", + "integrity": "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.15.tgz", + "integrity": "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/types": "^3.973.7", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.17.tgz", + "integrity": "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "fast-xml-parser": "5.5.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hapi/accept": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-6.0.3.tgz", + "integrity": "sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/ammo": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/ammo/-/ammo-6.0.1.tgz", + "integrity": "sha512-pmL+nPod4g58kXrMcsGLp05O2jF4P2Q3GiL8qYV7nKYEh3cGf+rV4P5Jyi2Uq0agGhVU63GtaSAfBEZOlrJn9w==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/b64": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-6.0.1.tgz", + "integrity": "sha512-ZvjX4JQReUmBheeCq+S9YavcnMMHWqx3S0jHNXWIM1kQDxB9cyfSycpVvjfrKcIS8Mh5N3hmu/YKo4Iag9g2Kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/boom": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz", + "integrity": "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/bounce": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/bounce/-/bounce-3.0.2.tgz", + "integrity": "sha512-d0XmlTi3H9HFDHhQLjg4F4auL1EY3Wqj7j7/hGDhFFe6xAbnm3qiGrXeT93zZnPH8gH+SKAFYiRzu26xkXcH3g==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/bourne": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/call": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@hapi/call/-/call-9.0.1.tgz", + "integrity": "sha512-uPojQRqEL1GRZR4xXPqcLMujQGaEpyVPRyBlD8Pp5rqgIwLhtveF9PkixiKru2THXvuN8mUrLeet5fqxKAAMGg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/catbox": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@hapi/catbox/-/catbox-12.1.1.tgz", + "integrity": "sha512-hDqYB1J+R0HtZg4iPH3LEnldoaBsar6bYp0EonBmNQ9t5CO+1CqgCul2ZtFveW1ReA5SQuze9GPSU7/aecERhw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/podium": "^5.0.0", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/catbox-memory": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/catbox-memory/-/catbox-memory-6.0.2.tgz", + "integrity": "sha512-H1l4ugoFW/ZRkqeFrIo8p1rWN0PA4MDTfu4JmcoNDvnY975o29mqoZblqFTotxNHlEkMPpIiIBJTV+Mbi+aF0g==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/content": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/content/-/content-6.0.1.tgz", + "integrity": "sha512-lQ2vOoFMNYxwKVnKf+3Pi3PfoviM4EJYlT9JbrBPfEc0xKMiVDqqXF8UTE1S1oKhHQliWSP5t6zTKNlmaXBGcQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.0" + } + }, + "node_modules/@hapi/cryptiles": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-6.0.3.tgz", + "integrity": "sha512-r6VKalpbMHz4ci3gFjFysBmhwCg70RpYZy6OkjEpdXzAYnYFX5XsW7n4YMJvuIYpnMwLxGUjK/cBhA7X3JDvXw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/file": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/file/-/file-3.0.0.tgz", + "integrity": "sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hapi": { + "version": "21.4.8", + "resolved": "https://registry.npmjs.org/@hapi/hapi/-/hapi-21.4.8.tgz", + "integrity": "sha512-l93IrEG4iQyM+yKdngWmkPtajkJGM81yfinmSFmiaNHG+r1fgsWaewwcE1hhsFnqPrVZpU8Y3PiVJMb6uT+01Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/accept": "^6.0.3", + "@hapi/ammo": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/bounce": "^3.0.2", + "@hapi/call": "^9.0.1", + "@hapi/catbox": "^12.1.1", + "@hapi/catbox-memory": "^6.0.2", + "@hapi/heavy": "^8.0.1", + "@hapi/hoek": "^11.0.7", + "@hapi/mimos": "^7.0.1", + "@hapi/podium": "^5.0.2", + "@hapi/shot": "^6.0.2", + "@hapi/somever": "^4.1.1", + "@hapi/statehood": "^8.2.1", + "@hapi/subtext": "^8.1.2", + "@hapi/teamwork": "^6.0.1", + "@hapi/topo": "^6.0.2", + "@hapi/validate": "^2.0.1" + }, + "engines": { + "node": ">=14.15.0" + } + }, + "node_modules/@hapi/heavy": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@hapi/heavy/-/heavy-8.0.1.tgz", + "integrity": "sha512-gBD/NANosNCOp6RsYTsjo2vhr5eYA3BEuogk6cxY0QdhllkkTaJFYtTXv46xd6qhBVMbMMqcSdtqey+UQU3//w==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/iron": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@hapi/iron/-/iron-7.0.1.tgz", + "integrity": "sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/b64": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/cryptiles": "^6.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/mimos": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@hapi/mimos/-/mimos-7.0.1.tgz", + "integrity": "sha512-b79V+BrG0gJ9zcRx1VGcCI6r6GEzzZUgiGEJVoq5gwzuB2Ig9Cax8dUuBauQCFKvl2YWSWyOc8mZ8HDaJOtkew==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "mime-db": "^1.52.0" + } + }, + "node_modules/@hapi/nigel": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hapi/nigel/-/nigel-5.0.1.tgz", + "integrity": "sha512-uv3dtYuB4IsNaha+tigWmN8mQw/O9Qzl5U26Gm4ZcJVtDdB1AVJOwX3X5wOX+A07qzpEZnOMBAm8jjSqGsU6Nw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/vise": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/pez": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@hapi/pez/-/pez-6.1.1.tgz", + "integrity": "sha512-yg2OS1tC0S1sHXvhUtWsfRn6lrKl9jKtRhZ+EI0woOW/gqX5vM2PZ1459ypCvCYDRLJ9nIyueeEH5MJV1ZDqIg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/b64": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/content": "^6.0.1", + "@hapi/hoek": "^11.0.7", + "@hapi/nigel": "^5.0.1" + } + }, + "node_modules/@hapi/podium": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@hapi/podium/-/podium-5.0.2.tgz", + "integrity": "sha512-T7gf2JYHQQfEfewTQFbsaXoZxSvuXO/QBIGljucUQ/lmPnTTNAepoIKOakWNVWvo2fMEDjycu77r8k6dhreqHA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/teamwork": "^6.0.0", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/shot": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/shot/-/shot-6.0.2.tgz", + "integrity": "sha512-WKK1ShfJTrL1oXC0skoIZQYzvLsyMDEF8lfcWuQBjpjCN29qivr9U36ld1z0nt6edvzv28etNMOqUF4klnHryw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/somever": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@hapi/somever/-/somever-4.1.1.tgz", + "integrity": "sha512-lt3QQiDDOVRatS0ionFDNrDIv4eXz58IibQaZQDOg4DqqdNme8oa0iPWcE0+hkq/KTeBCPtEOjDOBKBKwDumVg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/bounce": "^3.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/statehood": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@hapi/statehood/-/statehood-8.2.1.tgz", + "integrity": "sha512-xf72TG/QINW26jUu+uL5H+crE1o8GplIgfPWwPZhnAGJzetIVAQEQYvzq+C0aEVHg5/lMMtQ+L9UryuSa5Yjkg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bounce": "^3.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/cryptiles": "^6.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/iron": "^7.0.1", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/subtext": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@hapi/subtext/-/subtext-8.1.2.tgz", + "integrity": "sha512-2x71YJHmFpCjhIhfiNZdKp63nh3xRPp7RrwH7JoO9R4Sd0DRzzRU/VfX2fMmUR7jcoS5qNET1WyGIaqKpMu/ng==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/content": "^6.0.1", + "@hapi/file": "^3.0.0", + "@hapi/hoek": "^11.0.7", + "@hapi/pez": "^6.1.1", + "@hapi/wreck": "^18.1.0" + } + }, + "node_modules/@hapi/teamwork": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/teamwork/-/teamwork-6.0.1.tgz", + "integrity": "sha512-52OXRslUfYwXAOG8k58f2h2ngXYQGP0x5RPOo+eWA/FtyLgHjGMrE3+e9LSXP/0q2YfHAK5wj9aA9DTy1K+kyQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/validate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", + "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/topo": "^6.0.1" + } + }, + "node_modules/@hapi/vise": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hapi/vise/-/vise-5.0.1.tgz", + "integrity": "sha512-XZYWzzRtINQLedPYlIkSkUr7m5Ddwlu99V9elh8CSygXstfv3UnWIXT0QD+wmR0VAG34d2Vx3olqcEhRRoTu9A==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/wreck": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.1.0.tgz", + "integrity": "sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", + "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", + "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.14", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.14.tgz", + "integrity": "sha512-N55f8mPEccpzKetUagdvmAy8oohf0J5cuj9jLI1TaSceRlq0pJsIZepY3kmAXAhyxqXPV6hDerDQhqQPKWgAoQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.4", + "@smithy/util-middleware": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.14", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.14.tgz", + "integrity": "sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-stream": "^4.5.22", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.13.tgz", + "integrity": "sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.13.tgz", + "integrity": "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.0", + "@smithy/util-hex-encoding": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.13.tgz", + "integrity": "sha512-wwybfcOX0tLqCcBP378TIU9IqrDuZq/tDV48LlZNydMpCnqnYr+hWBAYbRE+rFFf/p7IkDJySM3bgiMKP2ihPg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.13.tgz", + "integrity": "sha512-ied1lO559PtAsMJzg2TKRlctLnEi1PfkNeMMpdwXDImk1zV9uvS/Oxoy/vcy9uv1GKZAjDAB5xT6ziE9fzm5wA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.13.tgz", + "integrity": "sha512-hFyK+ORJrxAN3RYoaD6+gsGDQjeix8HOEkosoajvXYZ4VeqonM3G4jd9IIRm/sWGXUKmudkY9KdYjzosUqdM8A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.13.tgz", + "integrity": "sha512-kRrq4EKLGeOxhC2CBEhRNcu1KSzNJzYY7RK3S7CxMPgB5dRrv55WqQOtRwQxQLC04xqORFLUgnDlc6xrNUULaA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.16.tgz", + "integrity": "sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.13", + "@smithy/querystring-builder": "^4.2.13", + "@smithy/types": "^4.14.0", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.14.tgz", + "integrity": "sha512-rtQ5es8r/5v4rav7q5QTsfx9CtCyzrz/g7ZZZBH2xtMmd6G/KQrLOWfSHTvFOUPlVy59RQvxeBYJaLRoybMEyA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.2", + "@smithy/chunked-blob-reader-native": "^4.2.3", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.13.tgz", + "integrity": "sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.13.tgz", + "integrity": "sha512-WdQ7HwUjINXETeh6dqUeob1UHIYx8kAn9PSp1HhM2WWegiZBYVy2WXIs1lB07SZLan/udys9SBnQGt9MQbDpdg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.13.tgz", + "integrity": "sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.13.tgz", + "integrity": "sha512-cNm7I9NXolFxtS20ojROddOEpSAeI1Obq6pd1Kj5HtHws3s9Fkk8DdHDfQSs5KuxCewZuVK6UqrJnfJmiMzDuQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.13.tgz", + "integrity": "sha512-IPMLm/LE4AZwu6qiE8Rr8vJsWhs9AtOdySRXrOM7xnvclp77Tyh7hMs/FRrMf26kgIe67vFJXXOSmVxS7oKeig==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.29", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.29.tgz", + "integrity": "sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.14", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-middleware": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.0.tgz", + "integrity": "sha512-/NzISn4grj/BRFVua/xnQwF+7fakYZgimpw2dfmlPgcqecBMKxpB9g5mLYRrmBD5OrPoODokw4Vi1hrSR4zRyw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/service-error-classification": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.0", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.17", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.17.tgz", + "integrity": "sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.14", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.13.tgz", + "integrity": "sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.13.tgz", + "integrity": "sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.2.tgz", + "integrity": "sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.13", + "@smithy/querystring-builder": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.13.tgz", + "integrity": "sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.13.tgz", + "integrity": "sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.13.tgz", + "integrity": "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.13.tgz", + "integrity": "sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.13.tgz", + "integrity": "sha512-a0s8XZMfOC/qpqq7RCPvJlk93rWFrElH6O++8WJKz0FqnA4Y7fkNi/0mnGgSH1C4x6MFsuBA8VKu4zxFrMe5Vw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.8.tgz", + "integrity": "sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.13.tgz", + "integrity": "sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.9.tgz", + "integrity": "sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.14", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-stream": "^4.5.22", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.0.tgz", + "integrity": "sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.13.tgz", + "integrity": "sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.45", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.45.tgz", + "integrity": "sha512-ag9sWc6/nWZAuK3Wm9KlFJUnRkXLrXn33RFjIAmCTFThqLHY+7wCst10BGq56FxslsDrjhSie46c8OULS+BiIw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.49", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.49.tgz", + "integrity": "sha512-jlN6vHwE8gY5AfiFBavtD3QtCX2f7lM3BKkz7nFKSNfFR5nXLXLg6sqXTJEEyDwtxbztIDBQCfjsGVXlIru2lQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.14", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.4.tgz", + "integrity": "sha512-BKoR/ubPp9KNKFxPpg1J28N1+bgu8NGAtJblBP7yHy8yQPBWhIAv9+l92SlQLpolGm71CVO+btB60gTgzT0wog==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.13.tgz", + "integrity": "sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.0.tgz", + "integrity": "sha512-tSOPQNT/4KfbvqeMovWC3g23KSYy8czHd3tlN+tOYVNIDLSfxIsrPJihYi5TpNcoV789KWtgChUVedh2y6dDPg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.22", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.22.tgz", + "integrity": "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/types": "^4.14.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.15.tgz", + "integrity": "sha512-oUt9o7n8hBv3BL56sLSneL0XeigZSuem0Hr78JaoK33D9oKieyCvVP8eTSe3j7g2mm/S1DvzxKieG7JEWNJUNg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsoa/cli": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@tsoa/cli/-/cli-6.6.0.tgz", + "integrity": "sha512-thSW0EiqjkF7HspcPIVIy0ZX65VqbWALHbxwl8Sk83j2kakOMq+fJvfo8FcBAWlMki+JDH7CO5iaAaSLHbeqtg==", + "license": "MIT", + "dependencies": { + "@tsoa/runtime": "^6.6.0", + "@types/multer": "^1.4.12", + "fs-extra": "^11.2.0", + "glob": "^10.3.10", + "handlebars": "^4.7.8", + "merge-anything": "^5.1.7", + "minimatch": "^9.0.1", + "ts-deepmerge": "^7.0.2", + "typescript": "^5.7.2", + "validator": "^13.12.0", + "yaml": "^2.6.1", + "yargs": "^17.7.1" + }, + "bin": { + "tsoa": "dist/cli.js" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, + "node_modules/@tsoa/cli/node_modules/@types/multer": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", + "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@tsoa/cli/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tsoa/cli/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tsoa/cli/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tsoa/cli/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@tsoa/runtime": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@tsoa/runtime/-/runtime-6.6.0.tgz", + "integrity": "sha512-+rF2gdL8CX+jQ82/IBc+MRJFNAvWPoBBl77HHJv3ESVMqbKhlhlo97JHmKyFbLcX6XOJN8zl8gfQpAEJN4SOMQ==", + "license": "MIT", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hapi": "^21.3.12", + "@types/koa": "^2.15.0", + "@types/multer": "^1.4.12", + "express": "^4.21.2", + "reflect-metadata": "^0.2.2", + "validator": "^13.12.0" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, + "node_modules/@tsoa/runtime/node_modules/@types/multer": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", + "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@tsoa/runtime/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "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.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/@tsoa/runtime/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/@tsoa/runtime/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@tsoa/runtime/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@tsoa/runtime/node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@tsoa/runtime/node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@tsoa/runtime/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@tsoa/runtime/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@tsoa/runtime/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/@tsoa/runtime/node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@tsoa/runtime/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@tsoa/runtime/node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@tsoa/runtime/node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@tsoa/runtime/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/content-disposition": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz", + "integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==", + "license": "MIT" + }, + "node_modules/@types/cookies": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.2.tgz", + "integrity": "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-assert": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz", + "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "license": "MIT" + }, + "node_modules/@types/koa": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", + "license": "MIT", + "dependencies": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "node_modules/@types/koa-compose": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.9.tgz", + "integrity": "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==", + "license": "MIT", + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", + "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "devOptional": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", + "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-anything": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", + "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.4.0.tgz", + "integrity": "sha512-s4DQMxIdhj3jLFWd9LxHOplj4p9yQ4ffMGowFf3cpEgrrJjEhN0V5nxw4Ye1EViAGDoL4/1AeO6qHpqYPOzE4Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/swagger-ui-dist": { + "version": "5.32.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.2.tgz", + "integrity": "sha512-t6Ns52nS8LU2hqi0+rezMjFO1ZrCsCrnommXrU7Nfrg2va2dWahdvM6TuSwzdHpG29v6BHJyU1c/UWFhgVZzVQ==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-deepmerge": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-7.0.3.tgz", + "integrity": "sha512-Du/ZW2RfwV/D4cmA5rXafYjBQVuvu4qGiEEla4EmEHVHgRdx68Gftx7i66jn2bzHPwSVZY36Ae6OuDn9el4ZKA==", + "license": "ISC", + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "license": "MIT", + "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" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsoa": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/tsoa/-/tsoa-6.6.0.tgz", + "integrity": "sha512-7FudRojmbEpbSQ3t1pyG5EjV3scF7/X75giQt1q+tnuGjjJppB8BOEmIdCK/G8S5Dqnmpwz5Q3vxluKozpIW9A==", + "license": "MIT", + "dependencies": { + "@tsoa/cli": "^6.6.0", + "@tsoa/runtime": "^6.6.0" + }, + "bin": { + "tsoa": "dist/cli.js" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typeorm": { + "version": "0.3.28", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", + "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", + "license": "MIT", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "ansis": "^4.2.0", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "dayjs": "^1.11.19", + "debug": "^4.4.3", + "dedent": "^1.7.0", + "dotenv": "^16.6.1", + "glob": "^10.5.0", + "reflect-metadata": "^0.2.2", + "sha.js": "^2.4.12", + "sql-highlight": "^6.1.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@sap/hana-client": "^2.14.22", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..173a585 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "gem-track-backend", + "version": "1.0.0", + "private": true, + "description": "Industrial-grade backend starter using Node.js, TypeScript, TypeORM, TSOA, and Swagger.", + "main": "dist/main.js", + "scripts": { + "dev": "tsx watch src/main.ts", + "build": "npm run tsoa:spec && npm run tsoa:routes && tsc -p tsconfig.json", + "start": "node dist/main.js", + "typecheck": "tsc --noEmit", + "tsoa:spec": "tsoa spec", + "tsoa:routes": "tsoa routes" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.1026.0", + "@aws-sdk/lib-storage": "^3.1026.0", + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", + "multer": "^2.1.1", + "pg": "^8.20.0", + "reflect-metadata": "^0.2.2", + "swagger-ui-express": "^5.0.1", + "tsoa": "^6.6.0", + "typeorm": "^0.3.24" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.1", + "@types/jsonwebtoken": "^9.0.9", + "@types/multer": "^2.1.0", + "@types/node": "^22.13.10", + "@types/swagger-ui-express": "^4.1.8", + "ts-node": "^10.9.2", + "tsx": "^4.19.3", + "typescript": "^5.8.2" + } +} diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..e4a67ac --- /dev/null +++ b/src/app.ts @@ -0,0 +1,52 @@ +import cors from "cors"; +import express from "express"; + +import { getApplicationContext } from "./bootstrap/application-context"; +import { createAuthMiddleware } from "./shared/interfaces/http/middlewares/auth.middleware"; +import { + errorMiddleware, + notFoundMiddleware +} from "./shared/interfaces/http/middlewares/error.middleware"; +import { requestContextMiddleware } from "./shared/interfaces/http/middlewares/request-context.middleware"; +import { tenantMiddleware } from "./shared/interfaces/http/middlewares/tenant.middleware"; +import { RegisterRoutes } from "./shared/interfaces/http/tsoa/generated/routes"; +import { + createSwaggerRouter, + loadSwaggerDocument +} from "./shared/interfaces/http/tsoa/swagger"; + +export function createApp(): express.Express { + const context = getApplicationContext(); + const app = express(); + + app.use( + cors({ + origin: true, + credentials: true + }) + ); + app.use(express.json({ limit: "1mb" })); + app.use(express.urlencoded({ extended: true })); + + app.use(createAuthMiddleware(context.jwtService, context.logger)); + app.use( + tenantMiddleware(context.config.tenancy.mode, context.useCases.tenancy.resolveTenant) + ); + app.use(requestContextMiddleware); + + app.get("/swagger.json", (_request, response) => { + response.json(loadSwaggerDocument()); + }); + + app.use( + context.config.http.swaggerPath, + createSwaggerRouter("/swagger.json") + ); + + RegisterRoutes(app); + + app.use(notFoundMiddleware); + app.use(errorMiddleware(context.logger)); + + return app; +} diff --git a/src/bootstrap/application-context.ts b/src/bootstrap/application-context.ts new file mode 100644 index 0000000..94cdad2 --- /dev/null +++ b/src/bootstrap/application-context.ts @@ -0,0 +1,343 @@ +import { CreateUserHandler } from "../modules/identity-access/application/commands/create-user/create-user.handler"; +import { LoginHandler } from "../modules/identity-access/application/commands/login/login.handler"; +import { RegisterUserHandler } from "../modules/identity-access/application/commands/register-user/register-user.handler"; +import { GetCurrentUserHandler } from "../modules/identity-access/application/queries/get-current-user/get-current-user.handler"; +import { GetUserByIdHandler } from "../modules/identity-access/application/queries/get-user-by-id/get-user-by-id.handler"; +import { AccessTokenService } from "../modules/identity-access/application/services/access-token.service"; +import { AuthPrincipalFactory } from "../modules/identity-access/application/services/auth-principal.factory"; +import { TypeOrmRoleRepository } from "../modules/identity-access/infrastructure/persistence/typeorm/repositories/typeorm-role.repository"; +import { TypeOrmUserRepository } from "../modules/identity-access/infrastructure/persistence/typeorm/repositories/typeorm-user.repository"; +import { IdentityAccessSeeder } from "../modules/identity-access/infrastructure/services/identity-access-seeder"; +import { AddSubTaskHandler } from "../modules/task-management/application/commands/add-subtask/add-subtask.handler"; +import { CreateTaskHandler } from "../modules/task-management/application/commands/create-task/create-task.handler"; +import { DeleteSubTaskHandler } from "../modules/task-management/application/commands/delete-subtask/delete-subtask.handler"; +import { DeleteTaskHandler } from "../modules/task-management/application/commands/delete-task/delete-task.handler"; +import { UpdateSubTaskHandler } from "../modules/task-management/application/commands/update-subtask/update-subtask.handler"; +import { UpdateTaskHandler } from "../modules/task-management/application/commands/update-task/update-task.handler"; +import { GetTaskByIdHandler } from "../modules/task-management/application/queries/get-task-by-id/get-task-by-id.handler"; +import { ListTasksHandler } from "../modules/task-management/application/queries/list-tasks/list-tasks.handler"; +import { TypeOrmTaskRepository } from "../modules/task-management/infrastructure/persistence/typeorm/repositories/typeorm-task.repository"; +import { OnboardTenantHandler } from "../modules/tenancy/application/commands/onboard-tenant/onboard-tenant.handler"; +import { GetTenantByIdHandler } from "../modules/tenancy/application/queries/get-tenant-by-id/get-tenant-by-id.handler"; +import { ResolveTenantHandler } from "../modules/tenancy/application/queries/resolve-tenant/resolve-tenant.handler"; +import { TypeOrmTenantRepository } from "../modules/tenancy/infrastructure/persistence/typeorm/repositories/typeorm-tenant.repository"; +import { AssignRolesHandler } from "../modules/identity-access/application/commands/assign-roles/assign-roles.handler"; +import { InviteUserHandler } from "../modules/identity-access/application/commands/invite-user/invite-user.handler"; +import { UpdateUserStatusHandler } from "../modules/identity-access/application/commands/update-user-status/update-user-status.handler"; +import { ConsoleEmailSender } from "../modules/identity-access/infrastructure/services/console-email-sender"; +import { DeleteDocumentHandler } from "../modules/document-management/application/commands/delete-document/delete-document.handler"; +import { UpdateDocumentHandler } from "../modules/document-management/application/commands/update-document/update-document.handler"; +import { UploadDocumentHandler } from "../modules/document-management/application/commands/upload-document/upload-document.handler"; +import { DownloadDocumentHandler } from "../modules/document-management/application/queries/download-document/download-document.handler"; +import { TypeOrmSubscriptionPlanRepository } from "../modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-subscription-plan.repository"; +import { TypeOrmTenantSubscriptionRepository } from "../modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-tenant-subscription.repository"; +import { TypeOrmBillingRecordRepository } from "../modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-billing-record.repository"; +import { InitializeSubscriptionHandler } from "../modules/billing/application/commands/initialize-subscription/initialize-subscription.handler"; +import { ChangeSubscriptionPlanHandler } from "../modules/billing/application/commands/change-subscription-plan/change-subscription-plan.handler"; +import { GetTenantSubscriptionHandler } from "../modules/billing/application/queries/get-tenant-subscription/get-tenant-subscription.handler"; +import { GetBillingHistoryHandler } from "../modules/billing/application/queries/get-billing-history/get-billing-history.handler"; +import { BillingSeeder } from "../modules/billing/infrastructure/services/billing-seeder"; +import { MockPaymentProvider } from "../modules/billing/infrastructure/services/mock-payment-provider"; +import { GetDocumentByIdHandler } from "../modules/document-management/application/queries/get-document-by-id/get-document-by-id.handler"; +import { ListDocumentsHandler } from "../modules/document-management/application/queries/list-documents/list-documents.handler"; +import { DocumentLinkValidatorService } from "../modules/document-management/application/services/document-link-validator.service"; +import { TypeOrmDocumentRepository } from "../modules/document-management/infrastructure/persistence/typeorm/repositories/typeorm-document.repository"; +import { LocalDocumentStorageService } from "../modules/document-management/infrastructure/storage/local-document-storage.service"; +import { S3DocumentStorageService } from "../modules/document-management/infrastructure/storage/s3-document-storage.service"; +import { loadAppConfig } from "../shared/infrastructure/config/env"; +import { Logger } from "../shared/infrastructure/logging/logger"; +import { TypeOrmDataSourceFactory } from "../shared/infrastructure/persistence/typeorm/data-source.factory"; +import { TenantDataSourceResolver } from "../shared/infrastructure/persistence/typeorm/tenant-data-source.resolver"; +import { BcryptPasswordHasher } from "../shared/infrastructure/security/bcrypt-password-hasher"; +import { JwtService } from "../shared/infrastructure/security/jwt.service"; +import { NodeCryptoIdGenerator } from "../shared/infrastructure/system/node-crypto-id-generator"; +import { SystemClock } from "../shared/infrastructure/system/system-clock"; + +export interface ApplicationContext { + config: ReturnType; + logger: Logger; + jwtService: JwtService; + initialize: () => Promise; + shutdown: () => Promise; + useCases: { + tenancy: { + onboardTenant: OnboardTenantHandler; + getTenantById: GetTenantByIdHandler; + resolveTenant: ResolveTenantHandler; + }; + identityAccess: { + registerUser: RegisterUserHandler; + login: LoginHandler; + getCurrentUser: GetCurrentUserHandler; + createUser: CreateUserHandler; + inviteUser: InviteUserHandler; + assignRoles: AssignRolesHandler; + updateUserStatus: UpdateUserStatusHandler; + getUserById: GetUserByIdHandler; + }; + billing: { + initializeSubscription: InitializeSubscriptionHandler; + changeSubscriptionPlan: ChangeSubscriptionPlanHandler; + getTenantSubscription: GetTenantSubscriptionHandler; + getBillingHistory: GetBillingHistoryHandler; + }; + taskManagement: { + createTask: CreateTaskHandler; + updateTask: UpdateTaskHandler; + deleteTask: DeleteTaskHandler; + addSubTask: AddSubTaskHandler; + updateSubTask: UpdateSubTaskHandler; + deleteSubTask: DeleteSubTaskHandler; + getTaskById: GetTaskByIdHandler; + listTasks: ListTasksHandler; + }; + documentManagement: { + uploadDocument: UploadDocumentHandler; + updateDocument: UpdateDocumentHandler; + deleteDocument: DeleteDocumentHandler; + downloadDocument: DownloadDocumentHandler; + getDocumentById: GetDocumentByIdHandler; + listDocuments: ListDocumentsHandler; + }; + }; +} + +let applicationContext: ApplicationContext | null = null; + +export async function buildApplicationContext(): Promise { + const config = loadAppConfig(); + const logger = new Logger(config.application.name); + const dataSourceFactory = new TypeOrmDataSourceFactory(config, logger); + const tenantDataSourceResolver = new TenantDataSourceResolver( + dataSourceFactory + ); + + const tenantRepository = new TypeOrmTenantRepository(tenantDataSourceResolver); + const subscriptionPlanRepository = new TypeOrmSubscriptionPlanRepository(tenantDataSourceResolver); + const tenantSubscriptionRepository = new TypeOrmTenantSubscriptionRepository(tenantDataSourceResolver); + const billingRecordRepository = new TypeOrmBillingRecordRepository(tenantDataSourceResolver); + + const userRepository = new TypeOrmUserRepository(tenantDataSourceResolver); + const roleRepository = new TypeOrmRoleRepository(tenantDataSourceResolver); + const taskRepository = new TypeOrmTaskRepository(tenantDataSourceResolver); + const documentRepository = new TypeOrmDocumentRepository(tenantDataSourceResolver); + const identityAccessSeeder = new IdentityAccessSeeder( + tenantDataSourceResolver + ); + const tenantProvisioner = identityAccessSeeder; + const billingSeeder = new BillingSeeder( + subscriptionPlanRepository, + new SystemClock(), + new NodeCryptoIdGenerator() + ); + + const clock = new SystemClock(); + const idGenerator = new NodeCryptoIdGenerator(); + const jwtService = new JwtService({ + secret: config.security.jwtSecret, + expiresIn: config.security.jwtExpiresIn, + issuer: config.security.jwtIssuer, + audience: config.security.jwtAudience + }); + const passwordHasher = new BcryptPasswordHasher(config.security.bcryptRounds); + const authPrincipalFactory = new AuthPrincipalFactory(); + const accessTokenService = new AccessTokenService( + jwtService, + authPrincipalFactory + ); + + const emailSender = new ConsoleEmailSender(); + + const documentStorage = config.storage.provider === "s3" + ? new S3DocumentStorageService(config.storage.s3) + : new LocalDocumentStorageService(config.storage.local.basePath); + + const documentLinkValidator = new DocumentLinkValidatorService( + userRepository, + taskRepository, + idGenerator + ); + + const createUser = new CreateUserHandler( + userRepository, + roleRepository, + tenantRepository, + tenantSubscriptionRepository, + subscriptionPlanRepository, + passwordHasher, + clock, + idGenerator + ); + + const initializeSubscription = new InitializeSubscriptionHandler( + tenantSubscriptionRepository, + subscriptionPlanRepository, + clock, + idGenerator + ); + + const onboardTenant = new OnboardTenantHandler( + tenantRepository, + tenantProvisioner, + initializeSubscription, + clock, + idGenerator + ); + + const context: ApplicationContext = { + config, + logger, + jwtService, + initialize: async () => { + if (config.database.initializeOnBootstrap) { + await tenantDataSourceResolver.getPlatformDataSource(); + await billingSeeder.seed(); + } + + logger.info("Application context initialized", { + tenancyMode: config.tenancy.mode, + databaseInitialization: config.database.initializeOnBootstrap + ? "eager" + : "lazy" + }); + }, + shutdown: async () => { + await dataSourceFactory.destroy(); + logger.info("Application context stopped"); + }, + useCases: { + tenancy: { + onboardTenant: onboardTenant, + getTenantById: new GetTenantByIdHandler(tenantRepository), + resolveTenant: new ResolveTenantHandler(tenantRepository) + }, + identityAccess: { + registerUser: new RegisterUserHandler( + userRepository, + roleRepository, + tenantRepository, + passwordHasher, + clock, + idGenerator, + accessTokenService + ), + login: new LoginHandler( + userRepository, + tenantRepository, + passwordHasher, + accessTokenService + ), + getCurrentUser: new GetCurrentUserHandler(userRepository), + createUser: createUser, + inviteUser: new InviteUserHandler( + userRepository, + roleRepository, + tenantRepository, + tenantSubscriptionRepository, + subscriptionPlanRepository, + emailSender, + clock, + idGenerator + ), + assignRoles: new AssignRolesHandler( + userRepository, + roleRepository, + clock + ), + updateUserStatus: new UpdateUserStatusHandler( + userRepository, + clock + ), + getUserById: new GetUserByIdHandler(userRepository) + }, + billing: { + initializeSubscription, + changeSubscriptionPlan: new ChangeSubscriptionPlanHandler( + tenantSubscriptionRepository, + subscriptionPlanRepository, + billingRecordRepository, + clock, + idGenerator + ), + getTenantSubscription: new GetTenantSubscriptionHandler( + tenantSubscriptionRepository, + subscriptionPlanRepository + ), + getBillingHistory: new GetBillingHistoryHandler( + billingRecordRepository + ) + }, + taskManagement: { + createTask: new CreateTaskHandler( + taskRepository, + tenantRepository, + userRepository, + clock, + idGenerator + ), + updateTask: new UpdateTaskHandler( + taskRepository, + userRepository, + clock + ), + deleteTask: new DeleteTaskHandler(taskRepository), + addSubTask: new AddSubTaskHandler( + taskRepository, + userRepository, + clock, + idGenerator + ), + updateSubTask: new UpdateSubTaskHandler( + taskRepository, + userRepository, + clock + ), + deleteSubTask: new DeleteSubTaskHandler(taskRepository, clock), + getTaskById: new GetTaskByIdHandler(taskRepository), + listTasks: new ListTasksHandler(taskRepository) + }, + documentManagement: { + uploadDocument: new UploadDocumentHandler( + documentRepository, + tenantRepository, + userRepository, + documentStorage, + documentLinkValidator, + clock, + idGenerator + ), + updateDocument: new UpdateDocumentHandler( + documentRepository, + documentLinkValidator, + clock + ), + deleteDocument: new DeleteDocumentHandler( + documentRepository, + documentStorage + ), + downloadDocument: new DownloadDocumentHandler( + documentRepository, + documentStorage + ), + getDocumentById: new GetDocumentByIdHandler(documentRepository), + listDocuments: new ListDocumentsHandler(documentRepository) + } + } + }; + + return context; +} + +export function setApplicationContext(context: ApplicationContext): void { + applicationContext = context; +} + +export function getApplicationContext(): ApplicationContext { + if (!applicationContext) { + throw new Error( + "Application context has not been initialized. Call buildApplicationContext first." + ); + } + + return applicationContext; +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..90700a2 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,41 @@ +import "reflect-metadata"; + +import { createApp } from "./app"; +import { + buildApplicationContext, + setApplicationContext +} from "./bootstrap/application-context"; + +async function bootstrap(): Promise { + const context = await buildApplicationContext(); + setApplicationContext(context); + await context.initialize(); + + const app = createApp(); + const server = app.listen(context.config.http.port, () => { + context.logger.info("HTTP server started", { + port: context.config.http.port, + apiBasePath: context.config.http.apiBasePath, + swaggerPath: context.config.http.swaggerPath + }); + }); + + const shutdown = async (signal: string): Promise => { + context.logger.info("Shutdown signal received", { signal }); + + server.close(async () => { + await context.shutdown(); + process.exit(0); + }); + }; + + process.on("SIGINT", () => { + void shutdown("SIGINT"); + }); + + process.on("SIGTERM", () => { + void shutdown("SIGTERM"); + }); +} + +void bootstrap(); diff --git a/src/modules/billing/application/commands/change-subscription-plan/change-subscription-plan.command.ts b/src/modules/billing/application/commands/change-subscription-plan/change-subscription-plan.command.ts new file mode 100644 index 0000000..8434396 --- /dev/null +++ b/src/modules/billing/application/commands/change-subscription-plan/change-subscription-plan.command.ts @@ -0,0 +1,4 @@ +export interface ChangeSubscriptionPlanCommand { + tenantId: string; + planCode: string; +} diff --git a/src/modules/billing/application/commands/change-subscription-plan/change-subscription-plan.handler.ts b/src/modules/billing/application/commands/change-subscription-plan/change-subscription-plan.handler.ts new file mode 100644 index 0000000..b7577e3 --- /dev/null +++ b/src/modules/billing/application/commands/change-subscription-plan/change-subscription-plan.handler.ts @@ -0,0 +1,49 @@ +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { IdGeneratorPort } from "../../../../../shared/application/ports/id-generator.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { SubscriptionPlanRepository } from "../../../../billing/domain/repositories/subscription-plan.repository"; +import { TenantSubscriptionRepository } from "../../../../billing/domain/repositories/tenant-subscription.repository"; +import { ChangeSubscriptionPlanCommand } from "./change-subscription-plan.command"; +import { BillingRecordRepository } from "../../../domain/repositories/billing-record.repository"; +import { BillingRecordEntity } from "../../../domain/entities/billing-record.entity"; + +export class ChangeSubscriptionPlanHandler { + constructor( + private readonly subscriptionRepository: TenantSubscriptionRepository, + private readonly planRepository: SubscriptionPlanRepository, + private readonly billingRecordRepository: BillingRecordRepository, + private readonly clock: ClockPort, + private readonly idGenerator: IdGeneratorPort + ) {} + + public async execute(command: ChangeSubscriptionPlanCommand): Promise { + const subscription = await this.subscriptionRepository.findByTenantId(command.tenantId); + if (!subscription) { + throw new ApplicationError("Subscription not found", 404, "SUBSCRIPTION_NOT_FOUND"); + } + + const newPlan = await this.planRepository.findByCode(command.planCode); + if (!newPlan) { + throw new ApplicationError(`Plan with code ${command.planCode} not found`, 404, "PLAN_NOT_FOUND"); + } + + const now = this.clock.now(); + subscription.changePlan(newPlan.id, now); + + // Record billing entry if the plan has a price + if (newPlan.priceMonthly > 0) { + const billingRecord = BillingRecordEntity.create({ + id: this.idGenerator.generate(), + tenantId: command.tenantId, + subscriptionId: subscription.id, + amount: newPlan.priceMonthly, + currency: newPlan.currency, + status: "paid", // Auto-paid in this mock implementation + paidAt: now + }); + await this.billingRecordRepository.save(billingRecord); + } + + await this.subscriptionRepository.save(subscription); + } +} diff --git a/src/modules/billing/application/commands/initialize-subscription/initialize-subscription.command.ts b/src/modules/billing/application/commands/initialize-subscription/initialize-subscription.command.ts new file mode 100644 index 0000000..ad9dfc9 --- /dev/null +++ b/src/modules/billing/application/commands/initialize-subscription/initialize-subscription.command.ts @@ -0,0 +1,3 @@ +export interface InitializeSubscriptionCommand { + tenantId: string; +} diff --git a/src/modules/billing/application/commands/initialize-subscription/initialize-subscription.handler.ts b/src/modules/billing/application/commands/initialize-subscription/initialize-subscription.handler.ts new file mode 100644 index 0000000..49c60e8 --- /dev/null +++ b/src/modules/billing/application/commands/initialize-subscription/initialize-subscription.handler.ts @@ -0,0 +1,37 @@ +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { IdGeneratorPort } from "../../../../../shared/application/ports/id-generator.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { SubscriptionPlanRepository } from "../../../../billing/domain/repositories/subscription-plan.repository"; +import { TenantSubscriptionRepository } from "../../../../billing/domain/repositories/tenant-subscription.repository"; +import { TenantSubscriptionAggregate } from "../../../../billing/domain/aggregates/tenant-subscription.aggregate"; +import { InitializeSubscriptionCommand } from "./initialize-subscription.command"; + +export class InitializeSubscriptionHandler { + constructor( + private readonly subscriptionRepository: TenantSubscriptionRepository, + private readonly planRepository: SubscriptionPlanRepository, + private readonly clock: ClockPort, + private readonly idGenerator: IdGeneratorPort + ) {} + + public async execute(command: InitializeSubscriptionCommand): Promise { + const existing = await this.subscriptionRepository.findByTenantId(command.tenantId); + if (existing) { + return; // Already initialized + } + + const freePlan = await this.planRepository.findByCode("free"); + if (!freePlan) { + throw new ApplicationError("Default Free plan not found", 500, "BILLING_CONFIGURATION_ERROR"); + } + + const subscription = TenantSubscriptionAggregate.initialize({ + id: this.idGenerator.generate(), + tenantId: command.tenantId, + planId: freePlan.id, + now: this.clock.now() + }); + + await this.subscriptionRepository.save(subscription); + } +} diff --git a/src/modules/billing/application/ports/payment-provider.port.ts b/src/modules/billing/application/ports/payment-provider.port.ts new file mode 100644 index 0000000..84aa7c5 --- /dev/null +++ b/src/modules/billing/application/ports/payment-provider.port.ts @@ -0,0 +1,9 @@ +export interface PaymentProviderPort { + createCustomer(tenantId: string, email: string): Promise; + createSubscription(customerId: string, planCode: string): Promise<{ + subscriptionId: string; + status: string; + currentPeriodEnd: Date; + }>; + cancelSubscription(subscriptionId: string): Promise; +} diff --git a/src/modules/billing/application/queries/get-billing-history/get-billing-history.handler.ts b/src/modules/billing/application/queries/get-billing-history/get-billing-history.handler.ts new file mode 100644 index 0000000..bcafb0f --- /dev/null +++ b/src/modules/billing/application/queries/get-billing-history/get-billing-history.handler.ts @@ -0,0 +1,25 @@ +import { BillingRecordRepository } from "../../../domain/repositories/billing-record.repository"; +import { GetBillingHistoryQuery, BillingRecordResponseDto } from "./get-billing-history.query"; + +export class GetBillingHistoryHandler { + constructor( + private readonly billingRecordRepository: BillingRecordRepository + ) {} + + public async execute(query: GetBillingHistoryQuery): Promise { + const records = await this.billingRecordRepository.findByTenantId(query.tenantId); + + return records.map(record => { + const primitives = record.toPrimitives(); + return { + id: primitives.id, + amount: primitives.amount, + currency: primitives.currency, + status: primitives.status, + paidAt: primitives.paidAt?.toISOString(), + invoiceUrl: primitives.invoiceUrl, + createdAt: primitives.createdAt.toISOString() + }; + }).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + } +} diff --git a/src/modules/billing/application/queries/get-billing-history/get-billing-history.query.ts b/src/modules/billing/application/queries/get-billing-history/get-billing-history.query.ts new file mode 100644 index 0000000..5c8d4d9 --- /dev/null +++ b/src/modules/billing/application/queries/get-billing-history/get-billing-history.query.ts @@ -0,0 +1,13 @@ +export interface GetBillingHistoryQuery { + tenantId: string; +} + +export interface BillingRecordResponseDto { + id: string; + amount: number; + currency: string; + status: string; + paidAt?: string; + invoiceUrl?: string; + createdAt: string; +} diff --git a/src/modules/billing/application/queries/get-tenant-subscription/get-tenant-subscription.handler.ts b/src/modules/billing/application/queries/get-tenant-subscription/get-tenant-subscription.handler.ts new file mode 100644 index 0000000..1c2300e --- /dev/null +++ b/src/modules/billing/application/queries/get-tenant-subscription/get-tenant-subscription.handler.ts @@ -0,0 +1,33 @@ +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { SubscriptionPlanRepository } from "../../../domain/repositories/subscription-plan.repository"; +import { TenantSubscriptionRepository } from "../../../domain/repositories/tenant-subscription.repository"; +import { GetTenantSubscriptionQuery, TenantSubscriptionResponseDto } from "./get-tenant-subscription.query"; + +export class GetTenantSubscriptionHandler { + constructor( + private readonly subscriptionRepository: TenantSubscriptionRepository, + private readonly planRepository: SubscriptionPlanRepository + ) {} + + public async execute(query: GetTenantSubscriptionQuery): Promise { + const subscription = await this.subscriptionRepository.findByTenantId(query.tenantId); + if (!subscription) { + throw new ApplicationError("Subscription not found for this tenant", 404, "SUBSCRIPTION_NOT_FOUND"); + } + + const plan = await this.planRepository.findById(subscription.planId); + if (!plan) { + throw new ApplicationError("Plan not found", 500, "BILLING_CONFIGURATION_ERROR"); + } + + return { + planName: plan.name, + planCode: plan.code, + maxUsers: plan.maxUsers, + status: subscription.status, + currentPeriodEnd: subscription.currentPeriodEnd.toISOString(), + priceMonthly: plan.priceMonthly, + currency: plan.currency + }; + } +} diff --git a/src/modules/billing/application/queries/get-tenant-subscription/get-tenant-subscription.query.ts b/src/modules/billing/application/queries/get-tenant-subscription/get-tenant-subscription.query.ts new file mode 100644 index 0000000..af17d19 --- /dev/null +++ b/src/modules/billing/application/queries/get-tenant-subscription/get-tenant-subscription.query.ts @@ -0,0 +1,13 @@ +export interface GetTenantSubscriptionQuery { + tenantId: string; +} + +export interface TenantSubscriptionResponseDto { + planName: string; + planCode: string; + maxUsers: number; + status: string; + currentPeriodEnd: string; + priceMonthly: number; + currency: string; +} diff --git a/src/modules/billing/domain/aggregates/subscription-plan.aggregate.ts b/src/modules/billing/domain/aggregates/subscription-plan.aggregate.ts new file mode 100644 index 0000000..960abf8 --- /dev/null +++ b/src/modules/billing/domain/aggregates/subscription-plan.aggregate.ts @@ -0,0 +1,82 @@ +import { AggregateRoot } from "../../../../shared/domain/base/aggregate-root"; +import { DomainError } from "../../../../shared/domain/errors/domain-error"; + +export interface SubscriptionPlanPrimitives { + id: string; + code: string; + name: string; + maxUsers: number; + priceMonthly: number; + currency: string; + createdAt: Date; + updatedAt: Date; +} + +interface CreatePlanProps { + id: string; + code: string; + name: string; + maxUsers: number; + priceMonthly: number; + currency: string; + now: Date; +} + +export class SubscriptionPlanAggregate extends AggregateRoot { + private constructor(private readonly state: SubscriptionPlanPrimitives) { + super(state.id); + } + + public static create(props: CreatePlanProps): SubscriptionPlanAggregate { + if (!props.code.trim()) { + throw new DomainError("Plan code is required"); + } + + if (!props.name.trim()) { + throw new DomainError("Plan name is required"); + } + + if (props.maxUsers <= 0 && props.maxUsers !== -1) { + throw new DomainError("Max users must be positive or -1 for unlimited"); + } + + return new SubscriptionPlanAggregate({ + id: props.id, + code: props.code.trim().toLowerCase(), + name: props.name.trim(), + maxUsers: props.maxUsers, + priceMonthly: props.priceMonthly, + currency: props.currency.toUpperCase(), + createdAt: props.now, + updatedAt: props.now + }); + } + + public static rehydrate(primitives: SubscriptionPlanPrimitives): SubscriptionPlanAggregate { + return new SubscriptionPlanAggregate(primitives); + } + + public get code(): string { + return this.state.code; + } + + public get name(): string { + return this.state.name; + } + + public get maxUsers(): number { + return this.state.maxUsers; + } + + public get priceMonthly(): number { + return this.state.priceMonthly; + } + + public get currency(): string { + return this.state.currency; + } + + public toPrimitives(): SubscriptionPlanPrimitives { + return { ...this.state }; + } +} diff --git a/src/modules/billing/domain/aggregates/tenant-subscription.aggregate.ts b/src/modules/billing/domain/aggregates/tenant-subscription.aggregate.ts new file mode 100644 index 0000000..8847175 --- /dev/null +++ b/src/modules/billing/domain/aggregates/tenant-subscription.aggregate.ts @@ -0,0 +1,80 @@ +import { AggregateRoot } from "../../../../shared/domain/base/aggregate-root"; +import { DomainError } from "../../../../shared/domain/errors/domain-error"; + +export type SubscriptionStatus = "active" | "past_due" | "canceled"; + +export interface TenantSubscriptionPrimitives { + id: string; + tenantId: string; + planId: string; + status: SubscriptionStatus; + currentPeriodEnd: Date; + customerId?: string; + subscriptionId?: string; + createdAt: Date; + updatedAt: Date; +} + +interface InitializeSubscriptionProps { + id: string; + tenantId: string; + planId: string; + now: Date; +} + +export class TenantSubscriptionAggregate extends AggregateRoot { + private constructor(private readonly state: TenantSubscriptionPrimitives) { + super(state.id); + } + + public static initialize(props: InitializeSubscriptionProps): TenantSubscriptionAggregate { + return new TenantSubscriptionAggregate({ + id: props.id, + tenantId: props.tenantId, + planId: props.planId, + status: "active", + currentPeriodEnd: new Date(props.now.getTime() + 30 * 24 * 60 * 60 * 1000), // Default 30 days + createdAt: props.now, + updatedAt: props.now + }); + } + + public static rehydrate(primitives: TenantSubscriptionPrimitives): TenantSubscriptionAggregate { + return new TenantSubscriptionAggregate(primitives); + } + + public changePlan(newPlanId: string, now: Date): void { + if (this.state.planId === newPlanId) { + throw new DomainError("New plan must be different from the current plan"); + } + + this.state.planId = newPlanId; + this.state.updatedAt = now; + } + + public updateExternalIds(customerId: string, subscriptionId: string, now: Date): void { + this.state.customerId = customerId; + this.state.subscriptionId = subscriptionId; + this.state.updatedAt = now; + } + + public get tenantId(): string { + return this.state.tenantId; + } + + public get planId(): string { + return this.state.planId; + } + + public get status(): SubscriptionStatus { + return this.state.status; + } + + public get currentPeriodEnd(): Date { + return this.state.currentPeriodEnd; + } + + public toPrimitives(): TenantSubscriptionPrimitives { + return { ...this.state }; + } +} diff --git a/src/modules/billing/domain/entities/billing-record.entity.ts b/src/modules/billing/domain/entities/billing-record.entity.ts new file mode 100644 index 0000000..1a73cf2 --- /dev/null +++ b/src/modules/billing/domain/entities/billing-record.entity.ts @@ -0,0 +1,32 @@ +export type BillingStatus = "paid" | "failed" | "pending"; + +export interface BillingRecordPrimitives { + id: string; + tenantId: string; + subscriptionId: string; + amount: number; + currency: string; + status: BillingStatus; + paidAt?: Date; + invoiceUrl?: string; + createdAt: Date; +} + +export class BillingRecordEntity { + constructor(private readonly state: BillingRecordPrimitives) {} + + public static create(props: Omit): BillingRecordEntity { + return new BillingRecordEntity({ + ...props, + createdAt: new Date() + }); + } + + public toPrimitives(): BillingRecordPrimitives { + return { ...this.state }; + } + + public get amount(): number { return this.state.amount; } + public get currency(): string { return this.state.currency; } + public get status(): BillingStatus { return this.state.status; } +} diff --git a/src/modules/billing/domain/repositories/billing-record.repository.ts b/src/modules/billing/domain/repositories/billing-record.repository.ts new file mode 100644 index 0000000..3e35d05 --- /dev/null +++ b/src/modules/billing/domain/repositories/billing-record.repository.ts @@ -0,0 +1,6 @@ +import { BillingRecordEntity } from "../entities/billing-record.entity"; + +export interface BillingRecordRepository { + save(record: BillingRecordEntity): Promise; + findByTenantId(tenantId: string): Promise; +} diff --git a/src/modules/billing/domain/repositories/subscription-plan.repository.ts b/src/modules/billing/domain/repositories/subscription-plan.repository.ts new file mode 100644 index 0000000..b064ac6 --- /dev/null +++ b/src/modules/billing/domain/repositories/subscription-plan.repository.ts @@ -0,0 +1,8 @@ +import { SubscriptionPlanAggregate } from "../aggregates/subscription-plan.aggregate"; + +export interface SubscriptionPlanRepository { + save(plan: SubscriptionPlanAggregate): Promise; + findById(id: string): Promise; + findByCode(code: string): Promise; + findAll(): Promise; +} diff --git a/src/modules/billing/domain/repositories/tenant-subscription.repository.ts b/src/modules/billing/domain/repositories/tenant-subscription.repository.ts new file mode 100644 index 0000000..d100220 --- /dev/null +++ b/src/modules/billing/domain/repositories/tenant-subscription.repository.ts @@ -0,0 +1,7 @@ +import { TenantSubscriptionAggregate } from "../aggregates/tenant-subscription.aggregate"; + +export interface TenantSubscriptionRepository { + save(subscription: TenantSubscriptionAggregate): Promise; + findByTenantId(tenantId: string): Promise; + findById(id: string): Promise; +} diff --git a/src/modules/billing/infrastructure/persistence/typeorm/entities/billing-record.orm-entity.ts b/src/modules/billing/infrastructure/persistence/typeorm/entities/billing-record.orm-entity.ts new file mode 100644 index 0000000..9e47bbc --- /dev/null +++ b/src/modules/billing/infrastructure/persistence/typeorm/entities/billing-record.orm-entity.ts @@ -0,0 +1,31 @@ +import { Column, Entity, PrimaryColumn } from "typeorm"; + +@Entity({ name: "billing_records" }) +export class BillingRecordOrmEntity { + @PrimaryColumn("uuid") + id!: string; + + @Column({ type: "uuid", name: "tenant_id" }) + tenantId!: string; + + @Column({ type: "uuid", name: "subscription_id" }) + subscriptionId!: string; + + @Column({ type: "decimal", precision: 10, scale: 2 }) + amount!: number; + + @Column({ type: "varchar", length: 3 }) + currency!: string; + + @Column({ type: "varchar", length: 20 }) + status!: string; + + @Column({ type: "timestamptz", nullable: true, name: "paid_at" }) + paidAt?: Date; + + @Column({ type: "varchar", length: 255, nullable: true, name: "invoice_url" }) + invoiceUrl?: string; + + @Column({ type: "timestamptz", name: "created_at" }) + createdAt!: Date; +} diff --git a/src/modules/billing/infrastructure/persistence/typeorm/entities/subscription-plan.orm-entity.ts b/src/modules/billing/infrastructure/persistence/typeorm/entities/subscription-plan.orm-entity.ts new file mode 100644 index 0000000..fd01ab5 --- /dev/null +++ b/src/modules/billing/infrastructure/persistence/typeorm/entities/subscription-plan.orm-entity.ts @@ -0,0 +1,28 @@ +import { Column, Entity, PrimaryColumn } from "typeorm"; + +@Entity({ name: "subscription_plans" }) +export class SubscriptionPlanOrmEntity { + @PrimaryColumn("uuid") + id!: string; + + @Column({ type: "varchar", length: 64, unique: true }) + code!: string; + + @Column({ type: "varchar", length: 128 }) + name!: string; + + @Column({ type: "integer", name: "max_users" }) + maxUsers!: number; + + @Column({ type: "decimal", precision: 10, scale: 2, name: "price_monthly" }) + priceMonthly!: number; + + @Column({ type: "varchar", length: 3 }) + currency!: string; + + @Column({ type: "timestamptz", name: "created_at" }) + createdAt!: Date; + + @Column({ type: "timestamptz", name: "updated_at" }) + updatedAt!: Date; +} diff --git a/src/modules/billing/infrastructure/persistence/typeorm/entities/tenant-subscription.orm-entity.ts b/src/modules/billing/infrastructure/persistence/typeorm/entities/tenant-subscription.orm-entity.ts new file mode 100644 index 0000000..a23aaca --- /dev/null +++ b/src/modules/billing/infrastructure/persistence/typeorm/entities/tenant-subscription.orm-entity.ts @@ -0,0 +1,31 @@ +import { Column, Entity, PrimaryColumn } from "typeorm"; + +@Entity({ name: "tenant_subscriptions" }) +export class TenantSubscriptionOrmEntity { + @PrimaryColumn("uuid") + id!: string; + + @Column({ type: "uuid", name: "tenant_id", unique: true }) + tenantId!: string; + + @Column({ type: "uuid", name: "plan_id" }) + planId!: string; + + @Column({ type: "varchar", length: 20 }) + status!: string; + + @Column({ type: "timestamptz", name: "current_period_end" }) + currentPeriodEnd!: Date; + + @Column({ type: "varchar", length: 128, nullable: true, name: "customer_id" }) + customerId?: string; + + @Column({ type: "varchar", length: 128, nullable: true, name: "subscription_id" }) + subscriptionId?: string; + + @Column({ type: "timestamptz", name: "created_at" }) + createdAt!: Date; + + @Column({ type: "timestamptz", name: "updated_at" }) + updatedAt!: Date; +} diff --git a/src/modules/billing/infrastructure/persistence/typeorm/mappers/billing-record.mapper.ts b/src/modules/billing/infrastructure/persistence/typeorm/mappers/billing-record.mapper.ts new file mode 100644 index 0000000..9667494 --- /dev/null +++ b/src/modules/billing/infrastructure/persistence/typeorm/mappers/billing-record.mapper.ts @@ -0,0 +1,33 @@ +import { BillingStatus, BillingRecordEntity } from "../../../../domain/entities/billing-record.entity"; +import { BillingRecordOrmEntity } from "../entities/billing-record.orm-entity"; + +export class BillingRecordMapper { + public static toPersistence(record: BillingRecordEntity): BillingRecordOrmEntity { + const primitives = record.toPrimitives(); + const orm = new BillingRecordOrmEntity(); + orm.id = primitives.id; + orm.tenantId = primitives.tenantId; + orm.subscriptionId = primitives.subscriptionId; + orm.amount = primitives.amount; + orm.currency = primitives.currency; + orm.status = primitives.status; + orm.paidAt = primitives.paidAt; + orm.invoiceUrl = primitives.invoiceUrl; + orm.createdAt = primitives.createdAt; + return orm; + } + + public static toDomain(orm: BillingRecordOrmEntity): BillingRecordEntity { + return new BillingRecordEntity({ + id: orm.id, + tenantId: orm.tenantId, + subscriptionId: orm.subscriptionId, + amount: Number(orm.amount), + currency: orm.currency, + status: orm.status as BillingStatus, + paidAt: orm.paidAt, + invoiceUrl: orm.invoiceUrl, + createdAt: orm.createdAt + }); + } +} diff --git a/src/modules/billing/infrastructure/persistence/typeorm/mappers/subscription-plan.mapper.ts b/src/modules/billing/infrastructure/persistence/typeorm/mappers/subscription-plan.mapper.ts new file mode 100644 index 0000000..12bbc65 --- /dev/null +++ b/src/modules/billing/infrastructure/persistence/typeorm/mappers/subscription-plan.mapper.ts @@ -0,0 +1,31 @@ +import { SubscriptionPlanAggregate } from "../../../../domain/aggregates/subscription-plan.aggregate"; +import { SubscriptionPlanOrmEntity } from "../entities/subscription-plan.orm-entity"; + +export class SubscriptionPlanMapper { + public static toPersistence(plan: SubscriptionPlanAggregate): SubscriptionPlanOrmEntity { + const primitives = plan.toPrimitives(); + const orm = new SubscriptionPlanOrmEntity(); + orm.id = primitives.id; + orm.code = primitives.code; + orm.name = primitives.name; + orm.maxUsers = primitives.maxUsers; + orm.priceMonthly = primitives.priceMonthly; + orm.currency = primitives.currency; + orm.createdAt = primitives.createdAt; + orm.updatedAt = primitives.updatedAt; + return orm; + } + + public static toDomain(orm: SubscriptionPlanOrmEntity): SubscriptionPlanAggregate { + return SubscriptionPlanAggregate.rehydrate({ + id: orm.id, + code: orm.code, + name: orm.name, + maxUsers: orm.maxUsers, + priceMonthly: Number(orm.priceMonthly), + currency: orm.currency, + createdAt: orm.createdAt, + updatedAt: orm.updatedAt + }); + } +} diff --git a/src/modules/billing/infrastructure/persistence/typeorm/mappers/tenant-subscription.mapper.ts b/src/modules/billing/infrastructure/persistence/typeorm/mappers/tenant-subscription.mapper.ts new file mode 100644 index 0000000..ffb0278 --- /dev/null +++ b/src/modules/billing/infrastructure/persistence/typeorm/mappers/tenant-subscription.mapper.ts @@ -0,0 +1,33 @@ +import { SubscriptionStatus, TenantSubscriptionAggregate } from "../../../../domain/aggregates/tenant-subscription.aggregate"; +import { TenantSubscriptionOrmEntity } from "../entities/tenant-subscription.orm-entity"; + +export class TenantSubscriptionMapper { + public static toPersistence(subscription: TenantSubscriptionAggregate): TenantSubscriptionOrmEntity { + const primitives = subscription.toPrimitives(); + const orm = new TenantSubscriptionOrmEntity(); + orm.id = primitives.id; + orm.tenantId = primitives.tenantId; + orm.planId = primitives.planId; + orm.status = primitives.status; + orm.currentPeriodEnd = primitives.currentPeriodEnd; + orm.customerId = primitives.customerId; + orm.subscriptionId = primitives.subscriptionId; + orm.createdAt = primitives.createdAt; + orm.updatedAt = primitives.updatedAt; + return orm; + } + + public static toDomain(orm: TenantSubscriptionOrmEntity): TenantSubscriptionAggregate { + return TenantSubscriptionAggregate.rehydrate({ + id: orm.id, + tenantId: orm.tenantId, + planId: orm.planId, + status: orm.status as SubscriptionStatus, + currentPeriodEnd: orm.currentPeriodEnd, + customerId: orm.customerId, + subscriptionId: orm.subscriptionId, + createdAt: orm.createdAt, + updatedAt: orm.updatedAt + }); + } +} diff --git a/src/modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-billing-record.repository.ts b/src/modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-billing-record.repository.ts new file mode 100644 index 0000000..b07b2ed --- /dev/null +++ b/src/modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-billing-record.repository.ts @@ -0,0 +1,28 @@ +import { Repository as TypeOrmRepository } from "typeorm"; +import { TenantDataSourceResolver } from "../../../../../../shared/infrastructure/persistence/typeorm/tenant-data-source.resolver"; +import { BillingRecordEntity } from "../../../../domain/entities/billing-record.entity"; +import { BillingRecordRepository } from "../../../../domain/repositories/billing-record.repository"; +import { BillingRecordOrmEntity } from "../entities/billing-record.orm-entity"; +import { BillingRecordMapper } from "../mappers/billing-record.mapper"; + +export class TypeOrmBillingRecordRepository implements BillingRecordRepository { + constructor( + private readonly tenantDataSourceResolver: TenantDataSourceResolver + ) {} + + public async save(record: BillingRecordEntity): Promise { + const repository = await this.getRepository(); + await repository.save(BillingRecordMapper.toPersistence(record)); + } + + public async findByTenantId(tenantId: string): Promise { + const repository = await this.getRepository(); + const entities = await repository.findBy({ tenantId }); + return entities.map(BillingRecordMapper.toDomain); + } + + private async getRepository(): Promise> { + const dataSource = await this.tenantDataSourceResolver.getPlatformDataSource(); + return dataSource.getRepository(BillingRecordOrmEntity); + } +} diff --git a/src/modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-subscription-plan.repository.ts b/src/modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-subscription-plan.repository.ts new file mode 100644 index 0000000..3bec72a --- /dev/null +++ b/src/modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-subscription-plan.repository.ts @@ -0,0 +1,40 @@ +import { Repository as TypeOrmRepository } from "typeorm"; +import { TenantDataSourceResolver } from "../../../../../../shared/infrastructure/persistence/typeorm/tenant-data-source.resolver"; +import { SubscriptionPlanAggregate } from "../../../../domain/aggregates/subscription-plan.aggregate"; +import { SubscriptionPlanRepository } from "../../../../domain/repositories/subscription-plan.repository"; +import { SubscriptionPlanOrmEntity } from "../entities/subscription-plan.orm-entity"; +import { SubscriptionPlanMapper } from "../mappers/subscription-plan.mapper"; + +export class TypeOrmSubscriptionPlanRepository implements SubscriptionPlanRepository { + constructor( + private readonly tenantDataSourceResolver: TenantDataSourceResolver + ) {} + + public async save(plan: SubscriptionPlanAggregate): Promise { + const repository = await this.getRepository(); + await repository.save(SubscriptionPlanMapper.toPersistence(plan)); + } + + public async findById(id: string): Promise { + const repository = await this.getRepository(); + const entity = await repository.findOneBy({ id }); + return entity ? SubscriptionPlanMapper.toDomain(entity) : null; + } + + public async findByCode(code: string): Promise { + const repository = await this.getRepository(); + const entity = await repository.findOneBy({ code: code.toLowerCase() }); + return entity ? SubscriptionPlanMapper.toDomain(entity) : null; + } + + public async findAll(): Promise { + const repository = await this.getRepository(); + const entities = await repository.find(); + return entities.map(SubscriptionPlanMapper.toDomain); + } + + private async getRepository(): Promise> { + const dataSource = await this.tenantDataSourceResolver.getPlatformDataSource(); + return dataSource.getRepository(SubscriptionPlanOrmEntity); + } +} diff --git a/src/modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-tenant-subscription.repository.ts b/src/modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-tenant-subscription.repository.ts new file mode 100644 index 0000000..5170608 --- /dev/null +++ b/src/modules/billing/infrastructure/persistence/typeorm/repositories/typeorm-tenant-subscription.repository.ts @@ -0,0 +1,34 @@ +import { Repository as TypeOrmRepository } from "typeorm"; +import { TenantDataSourceResolver } from "../../../../../../shared/infrastructure/persistence/typeorm/tenant-data-source.resolver"; +import { TenantSubscriptionAggregate } from "../../../../domain/aggregates/tenant-subscription.aggregate"; +import { TenantSubscriptionRepository } from "../../../../domain/repositories/tenant-subscription.repository"; +import { TenantSubscriptionOrmEntity } from "../entities/tenant-subscription.orm-entity"; +import { TenantSubscriptionMapper } from "../mappers/tenant-subscription.mapper"; + +export class TypeOrmTenantSubscriptionRepository implements TenantSubscriptionRepository { + constructor( + private readonly tenantDataSourceResolver: TenantDataSourceResolver + ) {} + + public async save(subscription: TenantSubscriptionAggregate): Promise { + const repository = await this.getRepository(); + await repository.save(TenantSubscriptionMapper.toPersistence(subscription)); + } + + public async findByTenantId(tenantId: string): Promise { + const repository = await this.getRepository(); + const entity = await repository.findOneBy({ tenantId }); + return entity ? TenantSubscriptionMapper.toDomain(entity) : null; + } + + public async findById(id: string): Promise { + const repository = await this.getRepository(); + const entity = await repository.findOneBy({ id }); + return entity ? TenantSubscriptionMapper.toDomain(entity) : null; + } + + private async getRepository(): Promise> { + const dataSource = await this.tenantDataSourceResolver.getPlatformDataSource(); + return dataSource.getRepository(TenantSubscriptionOrmEntity); + } +} diff --git a/src/modules/billing/infrastructure/services/billing-seeder.ts b/src/modules/billing/infrastructure/services/billing-seeder.ts new file mode 100644 index 0000000..a972b39 --- /dev/null +++ b/src/modules/billing/infrastructure/services/billing-seeder.ts @@ -0,0 +1,50 @@ +import { ClockPort } from "../../../../shared/application/ports/clock.port"; +import { IdGeneratorPort } from "../../../../shared/application/ports/id-generator.port"; +import { SubscriptionPlanAggregate } from "../../domain/aggregates/subscription-plan.aggregate"; +import { SubscriptionPlanRepository } from "../../domain/repositories/subscription-plan.repository"; + +export class BillingSeeder { + constructor( + private readonly planRepository: SubscriptionPlanRepository, + private readonly clock: ClockPort, + private readonly idGenerator: IdGeneratorPort + ) {} + + public async seed(): Promise { + const defaultPlans = [ + { + code: "free", + name: "Free Plan", + maxUsers: 5, + priceMonthly: 0, + currency: "USD" + }, + { + code: "pro", + name: "Pro Plan", + maxUsers: 50, + priceMonthly: 29.99, + currency: "USD" + }, + { + code: "enterprise", + name: "Enterprise Plan", + maxUsers: -1, // Unlimited + priceMonthly: 99.99, + currency: "USD" + } + ]; + + for (const planData of defaultPlans) { + const existing = await this.planRepository.findByCode(planData.code); + if (!existing) { + const plan = SubscriptionPlanAggregate.create({ + id: this.idGenerator.generate(), + now: this.clock.now(), + ...planData + }); + await this.planRepository.save(plan); + } + } + } +} diff --git a/src/modules/billing/infrastructure/services/mock-payment-provider.ts b/src/modules/billing/infrastructure/services/mock-payment-provider.ts new file mode 100644 index 0000000..731e1af --- /dev/null +++ b/src/modules/billing/infrastructure/services/mock-payment-provider.ts @@ -0,0 +1,23 @@ +import { PaymentProviderPort } from "../../application/ports/payment-provider.port"; + +export class MockPaymentProvider implements PaymentProviderPort { + public async createCustomer(tenantId: string, email: string): Promise { + return `cus_mock_${tenantId.slice(0, 8)}`; + } + + public async createSubscription(customerId: string, planCode: string): Promise<{ + subscriptionId: string; + status: string; + currentPeriodEnd: Date; + }> { + return { + subscriptionId: `sub_mock_${planCode}_${Date.now()}`, + status: "active", + currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) + }; + } + + public async cancelSubscription(subscriptionId: string): Promise { + console.log(`Mock: Canceling subscription ${subscriptionId}`); + } +} diff --git a/src/modules/billing/interfaces/http/controllers/billing.controller.ts b/src/modules/billing/interfaces/http/controllers/billing.controller.ts new file mode 100644 index 0000000..cf3c6ad --- /dev/null +++ b/src/modules/billing/interfaces/http/controllers/billing.controller.ts @@ -0,0 +1,52 @@ +import { Body, Controller, Get, Patch, Post, Request, Route, Security, Tags } from "tsoa"; +import { GetTenantSubscriptionHandler } from "../../../application/queries/get-tenant-subscription/get-tenant-subscription.handler"; +import { ChangeSubscriptionPlanHandler } from "../../../application/commands/change-subscription-plan/change-subscription-plan.handler"; +import { GetBillingHistoryHandler } from "../../../application/queries/get-billing-history/get-billing-history.handler"; +import { TenantSubscriptionResponseDto } from "../../../application/queries/get-tenant-subscription/get-tenant-subscription.query"; +import { BillingRecordResponseDto } from "../../../application/queries/get-billing-history/get-billing-history.query"; + +interface ChangePlanRequestDto { + planCode: string; +} + +@Route("v1/billing") +@Tags("Billing") +@Security("bearerAuth") +export class BillingController extends Controller { + constructor( + private readonly getSubscriptionHandler: GetTenantSubscriptionHandler, + private readonly changePlanHandler: ChangeSubscriptionPlanHandler, + private readonly getHistoryHandler: GetBillingHistoryHandler + ) { + super(); + } + + @Get("subscription") + public async getSubscription( + @Request() request: any + ): Promise { + return this.getSubscriptionHandler.execute({ + tenantId: request.principal.tenantId + }); + } + + @Patch("subscription/plan") + public async changePlan( + @Request() request: any, + @Body() body: ChangePlanRequestDto + ): Promise { + await this.changePlanHandler.execute({ + tenantId: request.principal.tenantId, + planCode: body.planCode + }); + } + + @Get("history") + public async getHistory( + @Request() request: any + ): Promise { + return this.getHistoryHandler.execute({ + tenantId: request.principal.tenantId + }); + } +} diff --git a/src/modules/document-management/application/commands/delete-document/delete-document.command.ts b/src/modules/document-management/application/commands/delete-document/delete-document.command.ts new file mode 100644 index 0000000..658df5f --- /dev/null +++ b/src/modules/document-management/application/commands/delete-document/delete-document.command.ts @@ -0,0 +1,4 @@ +export interface DeleteDocumentCommand { + tenantId: string; + documentId: string; +} diff --git a/src/modules/document-management/application/commands/delete-document/delete-document.handler.ts b/src/modules/document-management/application/commands/delete-document/delete-document.handler.ts new file mode 100644 index 0000000..eb19a0e --- /dev/null +++ b/src/modules/document-management/application/commands/delete-document/delete-document.handler.ts @@ -0,0 +1,25 @@ +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { DocumentRepository } from "../../../domain/repositories/document.repository"; +import { DocumentStoragePort } from "../../ports/document-storage.port"; +import { DeleteDocumentCommand } from "./delete-document.command"; + +export class DeleteDocumentHandler { + constructor( + private readonly documentRepository: DocumentRepository, + private readonly documentStorage: DocumentStoragePort + ) {} + + public async execute(command: DeleteDocumentCommand): Promise { + const document = await this.documentRepository.findByIdForTenant( + command.tenantId, + command.documentId + ); + + if (!document) { + throw new ApplicationError("Document not found", 404, "DOCUMENT_NOT_FOUND"); + } + + await this.documentStorage.delete(command.tenantId, document.storageKey); + await this.documentRepository.deleteForTenant(command.tenantId, command.documentId); + } +} diff --git a/src/modules/document-management/application/commands/update-document/update-document.command.ts b/src/modules/document-management/application/commands/update-document/update-document.command.ts new file mode 100644 index 0000000..c83c110 --- /dev/null +++ b/src/modules/document-management/application/commands/update-document/update-document.command.ts @@ -0,0 +1,11 @@ +export interface UpdateDocumentCommand { + tenantId: string; + documentId: string; + title?: string; + description?: string | null; + metadata?: Record; + tags?: string[]; + linkedTaskIds?: string[]; + linkedSubTaskIds?: string[]; + linkedUserIds?: string[]; +} diff --git a/src/modules/document-management/application/commands/update-document/update-document.dto.ts b/src/modules/document-management/application/commands/update-document/update-document.dto.ts new file mode 100644 index 0000000..76c1876 --- /dev/null +++ b/src/modules/document-management/application/commands/update-document/update-document.dto.ts @@ -0,0 +1,14 @@ +import { DocumentResponseDto } from "../../mappers/document-response.mapper"; + +export class UpdateDocumentRequestDto { + tenantId!: string; + title?: string; + description?: string | null; + metadata?: Record; + tags?: string[]; + linkedTaskIds?: string[]; + linkedSubTaskIds?: string[]; + linkedUserIds?: string[]; +} + +export { DocumentResponseDto }; diff --git a/src/modules/document-management/application/commands/update-document/update-document.handler.ts b/src/modules/document-management/application/commands/update-document/update-document.handler.ts new file mode 100644 index 0000000..5b83913 --- /dev/null +++ b/src/modules/document-management/application/commands/update-document/update-document.handler.ts @@ -0,0 +1,50 @@ +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { DocumentRepository } from "../../../domain/repositories/document.repository"; +import { toDocumentResponse, DocumentResponseDto } from "../../mappers/document-response.mapper"; +import { DocumentLinkValidatorService } from "../../services/document-link-validator.service"; +import { UpdateDocumentCommand } from "./update-document.command"; + +export class UpdateDocumentHandler { + constructor( + private readonly documentRepository: DocumentRepository, + private readonly documentLinkValidator: DocumentLinkValidatorService, + private readonly clock: ClockPort + ) {} + + public async execute(command: UpdateDocumentCommand): Promise { + const document = await this.documentRepository.findByIdForTenant( + command.tenantId, + command.documentId + ); + + if (!document) { + throw new ApplicationError("Document not found", 404, "DOCUMENT_NOT_FOUND"); + } + + const now = this.clock.now(); + const links = await this.documentLinkValidator.buildLinks( + command.tenantId, + command.documentId, + { + linkedTaskIds: command.linkedTaskIds, + linkedSubTaskIds: command.linkedSubTaskIds, + linkedUserIds: command.linkedUserIds + }, + now + ); + + document.updateMetadata({ + title: command.title, + description: command.description, + metadata: command.metadata, + tags: command.tags, + links, + now + }); + + await this.documentRepository.save(document); + + return toDocumentResponse(document); + } +} diff --git a/src/modules/document-management/application/commands/upload-document/upload-document.command.ts b/src/modules/document-management/application/commands/upload-document/upload-document.command.ts new file mode 100644 index 0000000..d104dbe --- /dev/null +++ b/src/modules/document-management/application/commands/upload-document/upload-document.command.ts @@ -0,0 +1,17 @@ +export interface UploadDocumentCommand { + tenantId: string; + title?: string; + description?: string; + metadata?: Record; + tags?: string[]; + linkedTaskIds?: string[]; + linkedSubTaskIds?: string[]; + linkedUserIds?: string[]; + uploadedByUserId: string; + file: { + buffer: Buffer; + originalFileName: string; + mimeType: string; + sizeBytes: number; + }; +} diff --git a/src/modules/document-management/application/commands/upload-document/upload-document.dto.ts b/src/modules/document-management/application/commands/upload-document/upload-document.dto.ts new file mode 100644 index 0000000..574a8e3 --- /dev/null +++ b/src/modules/document-management/application/commands/upload-document/upload-document.dto.ts @@ -0,0 +1,17 @@ +import { UploadedBinary } from "../../ports/document-storage.port"; +import { DocumentResponseDto } from "../../mappers/document-response.mapper"; + +export class UploadDocumentRequestDto { + tenantId!: string; + title?: string; + description?: string; + metadata?: Record | string; + tags?: string[] | string; + linkedTaskIds?: string[] | string; + linkedSubTaskIds?: string[] | string; + linkedUserIds?: string[] | string; +} + +export interface UploadDocumentFileDto extends UploadedBinary {} + +export { DocumentResponseDto }; diff --git a/src/modules/document-management/application/commands/upload-document/upload-document.handler.ts b/src/modules/document-management/application/commands/upload-document/upload-document.handler.ts new file mode 100644 index 0000000..3536234 --- /dev/null +++ b/src/modules/document-management/application/commands/upload-document/upload-document.handler.ts @@ -0,0 +1,91 @@ +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { IdGeneratorPort } from "../../../../../shared/application/ports/id-generator.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { UserRepository } from "../../../../identity-access/domain/repositories/user.repository"; +import { TenantRepository } from "../../../../tenancy/domain/repositories/tenant.repository"; +import { DocumentAggregate } from "../../../domain/aggregates/document.aggregate"; +import { DocumentRepository } from "../../../domain/repositories/document.repository"; +import { toDocumentResponse, DocumentResponseDto } from "../../mappers/document-response.mapper"; +import { DocumentStoragePort, StoredDocumentDescriptor } from "../../ports/document-storage.port"; +import { DocumentLinkValidatorService } from "../../services/document-link-validator.service"; +import { UploadDocumentCommand } from "./upload-document.command"; + +export class UploadDocumentHandler { + constructor( + private readonly documentRepository: DocumentRepository, + private readonly tenantRepository: TenantRepository, + private readonly userRepository: UserRepository, + private readonly documentStorage: DocumentStoragePort, + private readonly documentLinkValidator: DocumentLinkValidatorService, + private readonly clock: ClockPort, + private readonly idGenerator: IdGeneratorPort + ) {} + + public async execute(command: UploadDocumentCommand): Promise { + const tenant = await this.tenantRepository.findById(command.tenantId); + + if (!tenant) { + throw new ApplicationError("Tenant not found", 404, "TENANT_NOT_FOUND"); + } + + const uploader = await this.userRepository.findByIdForTenant( + command.tenantId, + command.uploadedByUserId + ); + + if (!uploader) { + throw new ApplicationError("Uploader does not belong to the tenant", 403, "INVALID_UPLOADER"); + } + + const documentId = this.idGenerator.generate(); + const now = this.clock.now(); + const links = await this.documentLinkValidator.buildLinks( + command.tenantId, + documentId, + { + linkedTaskIds: command.linkedTaskIds, + linkedSubTaskIds: command.linkedSubTaskIds, + linkedUserIds: command.linkedUserIds + }, + now + ); + + let storedDocument: StoredDocumentDescriptor | undefined; + + try { + storedDocument = await this.documentStorage.upload( + command.tenantId, + documentId, + command.file + ); + + const document = DocumentAggregate.upload({ + id: documentId, + tenantId: command.tenantId, + title: command.title, + description: command.description, + originalFileName: command.file.originalFileName, + mimeType: command.file.mimeType, + sizeBytes: command.file.sizeBytes, + storageProvider: storedDocument.storageProvider, + storageKey: storedDocument.storageKey, + checksumSha256: storedDocument.checksumSha256, + uploadedByUserId: command.uploadedByUserId, + metadata: command.metadata, + tags: command.tags, + links, + now + }); + + await this.documentRepository.save(document); + + return toDocumentResponse(document); + } catch (error) { + if (storedDocument) { + await this.documentStorage.delete(command.tenantId, storedDocument.storageKey).catch(() => undefined); + } + + throw error; + } + } +} diff --git a/src/modules/document-management/application/mappers/document-input-parser.ts b/src/modules/document-management/application/mappers/document-input-parser.ts new file mode 100644 index 0000000..16477eb --- /dev/null +++ b/src/modules/document-management/application/mappers/document-input-parser.ts @@ -0,0 +1,93 @@ +import { ApplicationError } from "../../../../shared/domain/errors/application-error"; + +function parseMaybeJson(input: unknown): unknown { + if (typeof input !== "string") { + return input; + } + + const normalized = input.trim(); + + if (!normalized) { + return undefined; + } + + if ( + (normalized.startsWith("{") && normalized.endsWith("}")) || + (normalized.startsWith("[") && normalized.endsWith("]")) + ) { + try { + return JSON.parse(normalized) as unknown; + } catch { + throw new ApplicationError( + "Document payload contains invalid JSON", + 422, + "INVALID_DOCUMENT_METADATA" + ); + } + } + + return input; +} + +export function normalizeOptionalString(input: unknown): string | undefined { + if (input === undefined || input === null) { + return undefined; + } + + const normalized = String(input).trim(); + return normalized ? normalized : undefined; +} + +export function parseMetadata(input: unknown): Record | undefined { + const normalizedInput = parseMaybeJson(input); + + if (normalizedInput === undefined || normalizedInput === null) { + return undefined; + } + + if (typeof normalizedInput !== "object" || Array.isArray(normalizedInput)) { + throw new ApplicationError("metadata must be a key-value object", 422, "INVALID_DOCUMENT_METADATA"); + } + + return Object.entries(normalizedInput as Record).reduce>( + (accumulator, [key, value]) => { + const normalizedKey = key.trim(); + + if (!normalizedKey) { + return accumulator; + } + + accumulator[normalizedKey] = String(value); + return accumulator; + }, + {} + ); +} + +export function parseTags(input: unknown): string[] | undefined { + const normalizedInput = parseMaybeJson(input); + + if (normalizedInput === undefined || normalizedInput === null) { + return undefined; + } + + if (!Array.isArray(normalizedInput)) { + throw new ApplicationError("tags must be an array", 422, "INVALID_DOCUMENT_METADATA"); + } + + return normalizedInput.map((tag) => String(tag)); +} + +export function parseLinkedIds(input: unknown): string[] | undefined { + const normalizedInput = parseMaybeJson(input); + + if (normalizedInput === undefined || normalizedInput === null) { + return undefined; + } + + if (!Array.isArray(normalizedInput)) { + throw new ApplicationError("link arrays must be arrays", 422, "INVALID_DOCUMENT_METADATA"); + } + + return normalizedInput.map((value) => String(value)); +} diff --git a/src/modules/document-management/application/mappers/document-response.mapper.ts b/src/modules/document-management/application/mappers/document-response.mapper.ts new file mode 100644 index 0000000..ba6906c --- /dev/null +++ b/src/modules/document-management/application/mappers/document-response.mapper.ts @@ -0,0 +1,54 @@ +import { DocumentAggregate } from "../../domain/aggregates/document.aggregate"; +import { DocumentLinkTargetType } from "../../domain/entities/document-link.entity"; + +export class DocumentLinkResponseDto { + id!: string; + targetType!: DocumentLinkTargetType; + targetId!: string; + createdAt!: string; +} + +export class DocumentResponseDto { + id!: string; + tenantId!: string; + title!: string; + description?: string; + originalFileName!: string; + mimeType!: string; + sizeBytes!: number; + storageProvider!: string; + checksumSha256!: string; + uploadedByUserId!: string; + metadata!: Record; + tags!: string[]; + links!: DocumentLinkResponseDto[]; + createdAt!: string; + updatedAt!: string; +} + +export function toDocumentResponse(document: DocumentAggregate): DocumentResponseDto { + const primitives = document.toPrimitives(); + + return { + id: primitives.id, + tenantId: primitives.tenantId, + title: primitives.title, + description: primitives.description, + originalFileName: primitives.originalFileName, + mimeType: primitives.mimeType, + sizeBytes: primitives.sizeBytes, + storageProvider: primitives.storageProvider, + checksumSha256: primitives.checksumSha256, + uploadedByUserId: primitives.uploadedByUserId, + metadata: { ...primitives.metadata }, + tags: [...primitives.tags], + links: primitives.links.map((link) => ({ + id: link.id, + targetType: link.targetType, + targetId: link.targetId, + createdAt: link.createdAt.toISOString() + })), + createdAt: primitives.createdAt.toISOString(), + updatedAt: primitives.updatedAt.toISOString() + }; +} diff --git a/src/modules/document-management/application/ports/document-storage.port.ts b/src/modules/document-management/application/ports/document-storage.port.ts new file mode 100644 index 0000000..7a7219f --- /dev/null +++ b/src/modules/document-management/application/ports/document-storage.port.ts @@ -0,0 +1,22 @@ +export interface UploadedBinary { + buffer: Buffer; + originalFileName: string; + mimeType: string; + sizeBytes: number; +} + +export interface StoredDocumentDescriptor { + storageProvider: string; + storageKey: string; + checksumSha256: string; +} + +export interface DownloadedBinary { + buffer: Buffer; +} + +export interface DocumentStoragePort { + upload(tenantId: string, documentId: string, file: UploadedBinary): Promise; + download(tenantId: string, storageKey: string): Promise; + delete(tenantId: string, storageKey: string): Promise; +} diff --git a/src/modules/document-management/application/queries/download-document/download-document.handler.ts b/src/modules/document-management/application/queries/download-document/download-document.handler.ts new file mode 100644 index 0000000..59a7367 --- /dev/null +++ b/src/modules/document-management/application/queries/download-document/download-document.handler.ts @@ -0,0 +1,38 @@ +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { DocumentRepository } from "../../../domain/repositories/document.repository"; +import { DocumentStoragePort } from "../../ports/document-storage.port"; +import { DownloadDocumentQuery } from "./download-document.query"; + +export interface DownloadDocumentResult { + fileName: string; + mimeType: string; + sizeBytes: number; + buffer: Buffer; +} + +export class DownloadDocumentHandler { + constructor( + private readonly documentRepository: DocumentRepository, + private readonly documentStorage: DocumentStoragePort + ) {} + + public async execute(query: DownloadDocumentQuery): Promise { + const document = await this.documentRepository.findByIdForTenant( + query.tenantId, + query.documentId + ); + + if (!document) { + throw new ApplicationError("Document not found", 404, "DOCUMENT_NOT_FOUND"); + } + + const binary = await this.documentStorage.download(query.tenantId, document.storageKey); + + return { + fileName: document.originalFileName, + mimeType: document.mimeType, + sizeBytes: document.sizeBytes, + buffer: binary.buffer + }; + } +} diff --git a/src/modules/document-management/application/queries/download-document/download-document.query.ts b/src/modules/document-management/application/queries/download-document/download-document.query.ts new file mode 100644 index 0000000..acc20ab --- /dev/null +++ b/src/modules/document-management/application/queries/download-document/download-document.query.ts @@ -0,0 +1,4 @@ +export interface DownloadDocumentQuery { + tenantId: string; + documentId: string; +} diff --git a/src/modules/document-management/application/queries/get-document-by-id/get-document-by-id.handler.ts b/src/modules/document-management/application/queries/get-document-by-id/get-document-by-id.handler.ts new file mode 100644 index 0000000..fca3600 --- /dev/null +++ b/src/modules/document-management/application/queries/get-document-by-id/get-document-by-id.handler.ts @@ -0,0 +1,21 @@ +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { DocumentRepository } from "../../../domain/repositories/document.repository"; +import { toDocumentResponse, DocumentResponseDto } from "../../mappers/document-response.mapper"; +import { GetDocumentByIdQuery } from "./get-document-by-id.query"; + +export class GetDocumentByIdHandler { + constructor(private readonly documentRepository: DocumentRepository) {} + + public async execute(query: GetDocumentByIdQuery): Promise { + const document = await this.documentRepository.findByIdForTenant( + query.tenantId, + query.documentId + ); + + if (!document) { + throw new ApplicationError("Document not found", 404, "DOCUMENT_NOT_FOUND"); + } + + return toDocumentResponse(document); + } +} diff --git a/src/modules/document-management/application/queries/get-document-by-id/get-document-by-id.query.ts b/src/modules/document-management/application/queries/get-document-by-id/get-document-by-id.query.ts new file mode 100644 index 0000000..e4f5b31 --- /dev/null +++ b/src/modules/document-management/application/queries/get-document-by-id/get-document-by-id.query.ts @@ -0,0 +1,4 @@ +export interface GetDocumentByIdQuery { + tenantId: string; + documentId: string; +} diff --git a/src/modules/document-management/application/queries/list-documents/list-documents.handler.ts b/src/modules/document-management/application/queries/list-documents/list-documents.handler.ts new file mode 100644 index 0000000..472be6b --- /dev/null +++ b/src/modules/document-management/application/queries/list-documents/list-documents.handler.ts @@ -0,0 +1,17 @@ +import { DocumentRepository } from "../../../domain/repositories/document.repository"; +import { toDocumentResponse, DocumentResponseDto } from "../../mappers/document-response.mapper"; +import { ListDocumentsQuery } from "./list-documents.query"; + +export class ListDocumentsHandler { + constructor(private readonly documentRepository: DocumentRepository) {} + + public async execute(query: ListDocumentsQuery): Promise { + const documents = await this.documentRepository.listForTenant(query.tenantId, { + uploadedByUserId: query.uploadedByUserId, + targetType: query.targetType, + targetId: query.targetId + }); + + return documents.map(toDocumentResponse); + } +} diff --git a/src/modules/document-management/application/queries/list-documents/list-documents.query.ts b/src/modules/document-management/application/queries/list-documents/list-documents.query.ts new file mode 100644 index 0000000..ca64fc1 --- /dev/null +++ b/src/modules/document-management/application/queries/list-documents/list-documents.query.ts @@ -0,0 +1,8 @@ +import { DocumentLinkTargetType } from "../../../domain/entities/document-link.entity"; + +export interface ListDocumentsQuery { + tenantId: string; + uploadedByUserId?: string; + targetType?: DocumentLinkTargetType; + targetId?: string; +} diff --git a/src/modules/document-management/application/services/document-link-validator.service.ts b/src/modules/document-management/application/services/document-link-validator.service.ts new file mode 100644 index 0000000..a2f0356 --- /dev/null +++ b/src/modules/document-management/application/services/document-link-validator.service.ts @@ -0,0 +1,87 @@ +import { DocumentLinkEntity, DocumentLinkTargetType } from "../../domain/entities/document-link.entity"; +import { UserRepository } from "../../../identity-access/domain/repositories/user.repository"; +import { TaskRepository } from "../../../task-management/domain/repositories/task.repository"; +import { ApplicationError } from "../../../../shared/domain/errors/application-error"; +import { IdGeneratorPort } from "../../../../shared/application/ports/id-generator.port"; + +export interface DocumentLinkInput { + linkedTaskIds?: string[]; + linkedSubTaskIds?: string[]; + linkedUserIds?: string[]; +} + +export class DocumentLinkValidatorService { + constructor( + private readonly userRepository: UserRepository, + private readonly taskRepository: TaskRepository, + private readonly idGenerator: IdGeneratorPort + ) {} + + public async buildLinks( + tenantId: string, + documentId: string, + links: DocumentLinkInput | undefined, + now: Date + ): Promise { + const taskIds = uniqueValues(links?.linkedTaskIds); + const subTaskIds = uniqueValues(links?.linkedSubTaskIds); + const userIds = uniqueValues(links?.linkedUserIds); + + for (const taskId of taskIds) { + const task = await this.taskRepository.findByIdForTenant(tenantId, taskId); + + if (!task) { + throw new ApplicationError("Linked task does not exist in tenant", 422, "INVALID_DOCUMENT_LINK"); + } + } + + for (const subTaskId of subTaskIds) { + const task = await this.taskRepository.findBySubTaskIdForTenant(tenantId, subTaskId); + + if (!task) { + throw new ApplicationError("Linked subtask does not exist in tenant", 422, "INVALID_DOCUMENT_LINK"); + } + } + + for (const userId of userIds) { + const user = await this.userRepository.findByIdForTenant(tenantId, userId); + + if (!user) { + throw new ApplicationError("Linked user does not exist in tenant", 422, "INVALID_DOCUMENT_LINK"); + } + } + + return [ + ...taskIds.map((taskId) => + this.createLink(tenantId, documentId, "task", taskId, now) + ), + ...subTaskIds.map((subTaskId) => + this.createLink(tenantId, documentId, "subtask", subTaskId, now) + ), + ...userIds.map((userId) => + this.createLink(tenantId, documentId, "user", userId, now) + ) + ]; + } + + private createLink( + tenantId: string, + documentId: string, + targetType: DocumentLinkTargetType, + targetId: string, + now: Date + ): DocumentLinkEntity { + return DocumentLinkEntity.create({ + id: this.idGenerator.generate(), + tenantId, + documentId, + targetType, + targetId, + createdAt: now + }); + } +} + +function uniqueValues(values?: string[]): string[] { + return [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))]; +} diff --git a/src/modules/document-management/domain/aggregates/document.aggregate.ts b/src/modules/document-management/domain/aggregates/document.aggregate.ts new file mode 100644 index 0000000..308a1a6 --- /dev/null +++ b/src/modules/document-management/domain/aggregates/document.aggregate.ts @@ -0,0 +1,206 @@ +import { AggregateRoot } from "../../../../shared/domain/base/aggregate-root"; +import { DomainError } from "../../../../shared/domain/errors/domain-error"; +import { DocumentLinkEntity, DocumentLinkPrimitives, DocumentLinkTargetType } from "../entities/document-link.entity"; + +export interface DocumentPrimitives { + id: string; + tenantId: string; + title: string; + description?: string; + originalFileName: string; + mimeType: string; + sizeBytes: number; + storageProvider: string; + storageKey: string; + checksumSha256: string; + uploadedByUserId: string; + metadata: Record; + tags: string[]; + links: DocumentLinkPrimitives[]; + createdAt: Date; + updatedAt: Date; +} + +interface UploadDocumentProps { + id: string; + tenantId: string; + title?: string; + description?: string; + originalFileName: string; + mimeType: string; + sizeBytes: number; + storageProvider: string; + storageKey: string; + checksumSha256: string; + uploadedByUserId: string; + metadata?: Record; + tags?: string[]; + links?: DocumentLinkEntity[]; + now: Date; +} + +export class DocumentAggregate extends AggregateRoot { + private readonly linksState: DocumentLinkEntity[]; + + private constructor(private readonly state: DocumentPrimitives) { + super(state.id); + this.linksState = state.links.map((link) => new DocumentLinkEntity(link)); + } + + public static upload(props: UploadDocumentProps): DocumentAggregate { + const title = (props.title ?? props.originalFileName).trim(); + + if (!props.tenantId.trim()) { + throw new DomainError("Tenant id is required"); + } + + if (!props.uploadedByUserId.trim()) { + throw new DomainError("Uploader user id is required"); + } + + if (title.length < 1) { + throw new DomainError("Document title is required"); + } + + if (props.sizeBytes <= 0) { + throw new DomainError("Document size must be greater than zero"); + } + + if (!props.storageKey.trim()) { + throw new DomainError("Storage key is required"); + } + + return new DocumentAggregate({ + id: props.id, + tenantId: props.tenantId.trim(), + title, + description: props.description?.trim() || undefined, + originalFileName: props.originalFileName.trim(), + mimeType: props.mimeType.trim() || "application/octet-stream", + sizeBytes: props.sizeBytes, + storageProvider: props.storageProvider.trim(), + storageKey: props.storageKey.trim(), + checksumSha256: props.checksumSha256.trim(), + uploadedByUserId: props.uploadedByUserId.trim(), + metadata: normalizeMetadata(props.metadata), + tags: normalizeTags(props.tags), + links: (props.links ?? []).map((link) => link.toPrimitives()), + createdAt: props.now, + updatedAt: props.now + }); + } + + public static rehydrate(primitives: DocumentPrimitives): DocumentAggregate { + return new DocumentAggregate({ + ...primitives, + metadata: normalizeMetadata(primitives.metadata), + tags: normalizeTags(primitives.tags) + }); + } + + public updateMetadata(props: { + title?: string; + description?: string | null; + metadata?: Record; + tags?: string[]; + links?: DocumentLinkEntity[]; + now: Date; + }): void { + if (props.title !== undefined) { + const title = props.title.trim(); + + if (title.length < 1) { + throw new DomainError("Document title is required"); + } + + this.state.title = title; + } + + if (props.description !== undefined) { + this.state.description = props.description?.trim() || undefined; + } + + if (props.metadata !== undefined) { + this.state.metadata = normalizeMetadata(props.metadata); + } + + if (props.tags !== undefined) { + this.state.tags = normalizeTags(props.tags); + } + + if (props.links !== undefined) { + this.linksState.splice(0, this.linksState.length, ...props.links); + this.state.links = props.links.map((link) => link.toPrimitives()); + } + + this.state.updatedAt = props.now; + } + + public hasLink(targetType: DocumentLinkTargetType, targetId: string): boolean { + return this.linksState.some( + (link) => link.targetType === targetType && link.targetId === targetId + ); + } + + public get tenantId(): string { + return this.state.tenantId; + } + + public get storageKey(): string { + return this.state.storageKey; + } + + public get storageProvider(): string { + return this.state.storageProvider; + } + + public get mimeType(): string { + return this.state.mimeType; + } + + public get originalFileName(): string { + return this.state.originalFileName; + } + + public get sizeBytes(): number { + return this.state.sizeBytes; + } + + public get uploadedByUserId(): string { + return this.state.uploadedByUserId; + } + + public get links(): DocumentLinkEntity[] { + return [...this.linksState]; + } + + public toPrimitives(): DocumentPrimitives { + return { + ...this.state, + metadata: normalizeMetadata(this.state.metadata), + tags: normalizeTags(this.state.tags), + links: this.linksState.map((link) => link.toPrimitives()) + }; + } +} + +function normalizeMetadata(metadata?: Record): Record { + if (!metadata) { + return {}; + } + + return Object.entries(metadata).reduce>((accumulator, [key, value]) => { + const normalizedKey = key.trim(); + + if (!normalizedKey) { + return accumulator; + } + + accumulator[normalizedKey] = String(value); + return accumulator; + }, {}); +} + +function normalizeTags(tags?: string[]): string[] { + return [...new Set((tags ?? []).map((tag) => tag.trim()).filter(Boolean))]; +} diff --git a/src/modules/document-management/domain/entities/document-link.entity.ts b/src/modules/document-management/domain/entities/document-link.entity.ts new file mode 100644 index 0000000..bb16507 --- /dev/null +++ b/src/modules/document-management/domain/entities/document-link.entity.ts @@ -0,0 +1,55 @@ +import { Entity } from "../../../../shared/domain/base/entity"; +import { DomainError } from "../../../../shared/domain/errors/domain-error"; + +export type DocumentLinkTargetType = "task" | "subtask" | "user"; + +export interface DocumentLinkPrimitives { + id: string; + tenantId: string; + documentId: string; + targetType: DocumentLinkTargetType; + targetId: string; + createdAt: Date; +} + +interface CreateDocumentLinkProps { + id: string; + tenantId: string; + documentId: string; + targetType: DocumentLinkTargetType; + targetId: string; + createdAt: Date; +} + +export class DocumentLinkEntity extends Entity { + constructor(private readonly state: DocumentLinkPrimitives) { + super(state.id); + } + + public static create(props: CreateDocumentLinkProps): DocumentLinkEntity { + if (!props.targetId.trim()) { + throw new DomainError("Document link target id is required"); + } + + return new DocumentLinkEntity({ + id: props.id, + tenantId: props.tenantId.trim(), + documentId: props.documentId.trim(), + targetType: props.targetType, + targetId: props.targetId.trim(), + createdAt: props.createdAt + }); + } + + public get targetType(): DocumentLinkTargetType { + return this.state.targetType; + } + + public get targetId(): string { + return this.state.targetId; + } + + public toPrimitives(): DocumentLinkPrimitives { + return { ...this.state }; + } +} diff --git a/src/modules/document-management/domain/repositories/document.repository.ts b/src/modules/document-management/domain/repositories/document.repository.ts new file mode 100644 index 0000000..c3bab01 --- /dev/null +++ b/src/modules/document-management/domain/repositories/document.repository.ts @@ -0,0 +1,15 @@ +import { Repository } from "../../../../shared/domain/base/repository"; +import { DocumentLinkTargetType } from "../entities/document-link.entity"; +import { DocumentAggregate } from "../aggregates/document.aggregate"; + +export interface ListDocumentsFilter { + uploadedByUserId?: string; + targetType?: DocumentLinkTargetType; + targetId?: string; +} + +export interface DocumentRepository extends Repository { + findByIdForTenant(tenantId: string, documentId: string): Promise; + listForTenant(tenantId: string, filter?: ListDocumentsFilter): Promise; + deleteForTenant(tenantId: string, documentId: string): Promise; +} diff --git a/src/modules/document-management/infrastructure/persistence/typeorm/entities/document-link.orm-entity.ts b/src/modules/document-management/infrastructure/persistence/typeorm/entities/document-link.orm-entity.ts new file mode 100644 index 0000000..c7b50ae --- /dev/null +++ b/src/modules/document-management/infrastructure/persistence/typeorm/entities/document-link.orm-entity.ts @@ -0,0 +1,32 @@ +import { Column, Entity, Index, ManyToOne, PrimaryColumn, JoinColumn } from "typeorm"; + +import { DocumentOrmEntity } from "./document.orm-entity"; + +@Entity({ name: "document_links" }) +@Index(["tenantId", "documentId"]) +@Index(["tenantId", "targetType", "targetId"]) +export class DocumentLinkOrmEntity { + @PrimaryColumn("uuid") + id!: string; + + @Column({ type: "uuid", name: "tenant_id" }) + tenantId!: string; + + @Column({ type: "uuid", name: "document_id" }) + documentId!: string; + + @Column({ type: "varchar", length: 32, name: "target_type" }) + targetType!: string; + + @Column({ type: "uuid", name: "target_id" }) + targetId!: string; + + @Column({ type: "timestamptz", name: "created_at" }) + createdAt!: Date; + + @ManyToOne(() => DocumentOrmEntity, (document) => document.links, { + onDelete: "CASCADE" + }) + @JoinColumn({ name: "document_id" }) + document!: DocumentOrmEntity; +} diff --git a/src/modules/document-management/infrastructure/persistence/typeorm/entities/document.orm-entity.ts b/src/modules/document-management/infrastructure/persistence/typeorm/entities/document.orm-entity.ts new file mode 100644 index 0000000..65deb88 --- /dev/null +++ b/src/modules/document-management/infrastructure/persistence/typeorm/entities/document.orm-entity.ts @@ -0,0 +1,60 @@ +import { Column, Entity, Index, OneToMany, PrimaryColumn } from "typeorm"; + +import { DocumentLinkOrmEntity } from "./document-link.orm-entity"; + +@Entity({ name: "documents" }) +@Index(["tenantId", "uploadedByUserId"]) +@Index(["tenantId", "createdAt"]) +export class DocumentOrmEntity { + @PrimaryColumn("uuid") + id!: string; + + @Column({ type: "uuid", name: "tenant_id" }) + tenantId!: string; + + @Column({ type: "varchar", length: 255 }) + title!: string; + + @Column({ type: "text", nullable: true }) + description?: string; + + @Column({ type: "varchar", name: "original_file_name", length: 255 }) + originalFileName!: string; + + @Column({ type: "varchar", name: "mime_type", length: 255 }) + mimeType!: string; + + @Column({ type: "integer", name: "size_bytes" }) + sizeBytes!: number; + + @Column({ type: "varchar", name: "storage_provider", length: 50 }) + storageProvider!: string; + + @Column({ type: "varchar", name: "storage_key", length: 512, unique: true }) + storageKey!: string; + + @Column({ type: "varchar", name: "checksum_sha256", length: 64 }) + checksumSha256!: string; + + @Column({ type: "uuid", name: "uploaded_by_user_id" }) + uploadedByUserId!: string; + + @Column({ type: "jsonb", default: () => "'{}'::jsonb" }) + metadata!: Record; + + @Column({ type: "jsonb", default: () => "'[]'::jsonb" }) + tags!: string[]; + + @Column({ type: "timestamptz", name: "created_at" }) + createdAt!: Date; + + @Column({ type: "timestamptz", name: "updated_at" }) + updatedAt!: Date; + + @OneToMany(() => DocumentLinkOrmEntity, (link) => link.document, { + cascade: true, + eager: true, + orphanedRowAction: "delete" + }) + links!: DocumentLinkOrmEntity[]; +} diff --git a/src/modules/document-management/infrastructure/persistence/typeorm/mappers/document-persistence.mapper.ts b/src/modules/document-management/infrastructure/persistence/typeorm/mappers/document-persistence.mapper.ts new file mode 100644 index 0000000..57c76d8 --- /dev/null +++ b/src/modules/document-management/infrastructure/persistence/typeorm/mappers/document-persistence.mapper.ts @@ -0,0 +1,69 @@ +import { DocumentAggregate } from "../../../../domain/aggregates/document.aggregate"; +import { DocumentLinkOrmEntity } from "../entities/document-link.orm-entity"; +import { DocumentOrmEntity } from "../entities/document.orm-entity"; + +export function toDocumentDomain(entity: DocumentOrmEntity): DocumentAggregate { + return DocumentAggregate.rehydrate({ + id: entity.id, + tenantId: entity.tenantId, + title: entity.title, + description: entity.description, + originalFileName: entity.originalFileName, + mimeType: entity.mimeType, + sizeBytes: entity.sizeBytes, + storageProvider: entity.storageProvider, + storageKey: entity.storageKey, + checksumSha256: entity.checksumSha256, + uploadedByUserId: entity.uploadedByUserId, + metadata: entity.metadata ?? {}, + tags: entity.tags ?? [], + links: (entity.links ?? []).map((link) => ({ + id: link.id, + tenantId: link.tenantId, + documentId: link.documentId, + targetType: link.targetType === "user" + ? "user" + : link.targetType === "subtask" + ? "subtask" + : "task", + targetId: link.targetId, + createdAt: link.createdAt + })), + createdAt: entity.createdAt, + updatedAt: entity.updatedAt + }); +} + +export function toDocumentPersistence(document: DocumentAggregate): DocumentOrmEntity { + const primitives = document.toPrimitives(); + const entity = new DocumentOrmEntity(); + + entity.id = primitives.id; + entity.tenantId = primitives.tenantId; + entity.title = primitives.title; + entity.description = primitives.description; + entity.originalFileName = primitives.originalFileName; + entity.mimeType = primitives.mimeType; + entity.sizeBytes = primitives.sizeBytes; + entity.storageProvider = primitives.storageProvider; + entity.storageKey = primitives.storageKey; + entity.checksumSha256 = primitives.checksumSha256; + entity.uploadedByUserId = primitives.uploadedByUserId; + entity.metadata = primitives.metadata; + entity.tags = primitives.tags; + entity.createdAt = primitives.createdAt; + entity.updatedAt = primitives.updatedAt; + entity.links = primitives.links.map((link) => { + const linkEntity = new DocumentLinkOrmEntity(); + linkEntity.id = link.id; + linkEntity.tenantId = link.tenantId; + linkEntity.documentId = link.documentId; + linkEntity.targetType = link.targetType; + linkEntity.targetId = link.targetId; + linkEntity.createdAt = link.createdAt; + linkEntity.document = entity; + return linkEntity; + }); + + return entity; +} diff --git a/src/modules/document-management/infrastructure/persistence/typeorm/repositories/typeorm-document.repository.ts b/src/modules/document-management/infrastructure/persistence/typeorm/repositories/typeorm-document.repository.ts new file mode 100644 index 0000000..21e9e42 --- /dev/null +++ b/src/modules/document-management/infrastructure/persistence/typeorm/repositories/typeorm-document.repository.ts @@ -0,0 +1,87 @@ +import { Repository as TypeOrmRepository } from "typeorm"; + +import { TenantDataSourceResolver } from "../../../../../../shared/infrastructure/persistence/typeorm/tenant-data-source.resolver"; +import { DocumentAggregate } from "../../../../domain/aggregates/document.aggregate"; +import { ListDocumentsFilter, DocumentRepository } from "../../../../domain/repositories/document.repository"; +import { DocumentOrmEntity } from "../entities/document.orm-entity"; +import { toDocumentDomain, toDocumentPersistence } from "../mappers/document-persistence.mapper"; + +export class TypeOrmDocumentRepository implements DocumentRepository { + constructor( + private readonly tenantDataSourceResolver: TenantDataSourceResolver + ) {} + + public async findById(id: string): Promise { + const repository = await this.getRepository(); + const entity = await repository.findOne({ + where: { id }, + relations: { links: true } + }); + + return entity ? toDocumentDomain(entity) : null; + } + + public async findByIdForTenant( + tenantId: string, + documentId: string + ): Promise { + const repository = await this.getRepository(); + const entity = await repository.findOne({ + where: { id: documentId, tenantId }, + relations: { links: true } + }); + + return entity ? toDocumentDomain(entity) : null; + } + + public async listForTenant( + tenantId: string, + filter?: ListDocumentsFilter + ): Promise { + const repository = await this.getRepository(); + const queryBuilder = repository + .createQueryBuilder("document") + .leftJoinAndSelect("document.links", "link") + .where("document.tenant_id = :tenantId", { tenantId }); + + if (filter?.uploadedByUserId) { + queryBuilder.andWhere("document.uploaded_by_user_id = :uploadedByUserId", { + uploadedByUserId: filter.uploadedByUserId + }); + } + + if (filter?.targetType) { + queryBuilder.andWhere("link.target_type = :targetType", { + targetType: filter.targetType + }); + } + + if (filter?.targetId) { + queryBuilder.andWhere("link.target_id = :targetId", { + targetId: filter.targetId + }); + } + + queryBuilder + .orderBy("document.created_at", "DESC") + .addOrderBy("link.created_at", "ASC"); + + const entities = await queryBuilder.getMany(); + return entities.map(toDocumentDomain); + } + + public async save(aggregate: DocumentAggregate): Promise { + const repository = await this.getRepository(); + await repository.save(toDocumentPersistence(aggregate)); + } + + public async deleteForTenant(tenantId: string, documentId: string): Promise { + const repository = await this.getRepository(); + await repository.delete({ id: documentId, tenantId }); + } + + private async getRepository(): Promise> { + const dataSource = await this.tenantDataSourceResolver.getPlatformDataSource(); + return dataSource.getRepository(DocumentOrmEntity); + } +} diff --git a/src/modules/document-management/infrastructure/storage/local-document-storage.service.ts b/src/modules/document-management/infrastructure/storage/local-document-storage.service.ts new file mode 100644 index 0000000..ce0f0d0 --- /dev/null +++ b/src/modules/document-management/infrastructure/storage/local-document-storage.service.ts @@ -0,0 +1,54 @@ +import { createHash } from "crypto"; +import { promises as fs } from "fs"; +import path from "path"; + +import { AppConfig } from "../../../../shared/infrastructure/config/app-config"; +import { DocumentStoragePort, DownloadedBinary, StoredDocumentDescriptor, UploadedBinary } from "../../application/ports/document-storage.port"; + +export class LocalDocumentStorageService implements DocumentStoragePort { + private readonly rootPath: string; + + constructor(private readonly basePath: string) { + this.rootPath = path.resolve(process.cwd(), basePath); + } + + public async upload( + tenantId: string, + documentId: string, + file: UploadedBinary + ): Promise { + const sanitizedFileName = sanitizeFileName(file.originalFileName); + const storageKey = path.posix.join(tenantId, documentId, sanitizedFileName); + const fullPath = this.resolveAbsolutePath(storageKey); + + await fs.mkdir(path.dirname(fullPath), { recursive: true }); + await fs.writeFile(fullPath, file.buffer); + + return { + storageProvider: "local", + storageKey, + checksumSha256: createHash("sha256").update(file.buffer).digest("hex") + }; + } + + public async download(_tenantId: string, storageKey: string): Promise { + const fullPath = this.resolveAbsolutePath(storageKey); + const buffer = await fs.readFile(fullPath); + + return { buffer }; + } + + public async delete(_tenantId: string, storageKey: string): Promise { + const fullPath = this.resolveAbsolutePath(storageKey); + await fs.rm(fullPath, { force: true }); + } + + private resolveAbsolutePath(storageKey: string): string { + return path.resolve(this.rootPath, storageKey.split("/").join(path.sep)); + } +} + +function sanitizeFileName(fileName: string): string { + const normalized = fileName.trim().replace(/[^a-zA-Z0-9._-]+/g, "-"); + return normalized || "document.bin"; +} diff --git a/src/modules/document-management/infrastructure/storage/s3-document-storage.service.ts b/src/modules/document-management/infrastructure/storage/s3-document-storage.service.ts new file mode 100644 index 0000000..9f10cd5 --- /dev/null +++ b/src/modules/document-management/infrastructure/storage/s3-document-storage.service.ts @@ -0,0 +1,93 @@ +import { createHash } from "crypto"; +import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3"; +import { Upload } from "@aws-sdk/lib-storage"; + +import { DocumentStoragePort, DownloadedBinary, StoredDocumentDescriptor, UploadedBinary } from "../../application/ports/document-storage.port"; + +export interface S3StorageConfig { + region: string; + bucket: string; + endpoint?: string; + accessKeyId?: string; + secretAccessKey?: string; + forcePathStyle?: boolean; +} + +export class S3DocumentStorageService implements DocumentStoragePort { + private readonly client: S3Client; + private readonly bucket: string; + + constructor(config: S3StorageConfig) { + this.bucket = config.bucket; + this.client = new S3Client({ + region: config.region, + endpoint: config.endpoint, + credentials: config.accessKeyId && config.secretAccessKey ? { + accessKeyId: config.accessKeyId, + secretAccessKey: config.secretAccessKey + } : undefined, + forcePathStyle: config.forcePathStyle + }); + } + + public async upload(tenantId: string, documentId: string, file: UploadedBinary): Promise { + const key = `${tenantId}/${documentId}-${file.originalFileName}`; + const checksum = createHash("sha256").update(file.buffer).digest("hex"); + + const upload = new Upload({ + client: this.client, + params: { + Bucket: this.bucket, + Key: key, + Body: file.buffer, + ContentType: file.mimeType, + Metadata: { + "original-name": file.originalFileName, + "tenant-id": tenantId + } + } + }); + + await upload.done(); + + return { + storageProvider: "s3", + storageKey: key, + checksumSha256: checksum + }; + } + + public async download(tenantId: string, storageKey: string): Promise { + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: storageKey + }); + + const response = await this.client.send(command); + + if (!response.Body) { + throw new Error("Empty response body from S3"); + } + + const buffer = Buffer.from(await response.Body.transformToByteArray()); + + return { + buffer + }; + } + + public async delete(tenantId: string, storageKey: string): Promise { + const command = new DeleteObjectCommand({ + Bucket: this.bucket, + Key: storageKey + }); + + await this.client.send(command).catch((error) => { + // If file doesn't exist, we consider it deleted + if (error.name === "NoSuchKey") { + return; + } + throw error; + }); + } +} diff --git a/src/modules/document-management/interfaces/http/controllers/documents.controller.ts b/src/modules/document-management/interfaces/http/controllers/documents.controller.ts new file mode 100644 index 0000000..7935ce0 --- /dev/null +++ b/src/modules/document-management/interfaces/http/controllers/documents.controller.ts @@ -0,0 +1,148 @@ +import type { Request as ExpressRequest } from "express"; +import { Body, Controller, Delete, FormField, Get, Patch, Path, Post, Query, Request, Route, Security, SuccessResponse, Tags, UploadedFile } from "tsoa"; + +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { DownloadDocumentHandler, DownloadDocumentResult } from "../../../application/queries/download-document/download-document.handler"; +import { GetDocumentByIdHandler } from "../../../application/queries/get-document-by-id/get-document-by-id.handler"; +import { ListDocumentsHandler } from "../../../application/queries/list-documents/list-documents.handler"; +import { UploadDocumentFileDto, UploadDocumentRequestDto } from "../../../application/commands/upload-document/upload-document.dto"; +import { UploadDocumentHandler } from "../../../application/commands/upload-document/upload-document.handler"; +import { UpdateDocumentRequestDto } from "../../../application/commands/update-document/update-document.dto"; +import { UpdateDocumentHandler } from "../../../application/commands/update-document/update-document.handler"; +import { DeleteDocumentHandler } from "../../../application/commands/delete-document/delete-document.handler"; +import { DocumentResponseDto } from "../../../application/mappers/document-response.mapper"; +import { normalizeOptionalString, parseLinkedIds, parseMetadata, parseTags } from "../../../application/mappers/document-input-parser"; +import { DocumentLinkTargetType } from "../../../domain/entities/document-link.entity"; + +@Route("v1/documents") +@Tags("Documents") +export class DocumentsController extends Controller { + constructor( + private readonly uploadDocumentHandler: UploadDocumentHandler, + private readonly updateDocumentHandler: UpdateDocumentHandler, + private readonly deleteDocumentHandler: DeleteDocumentHandler, + private readonly getDocumentByIdHandler: GetDocumentByIdHandler, + private readonly listDocumentsHandler: ListDocumentsHandler, + private readonly downloadDocumentHandler: DownloadDocumentHandler + ) { + super(); + } + + @Post("upload") + @Security("bearerAuth", ["documents:create"]) + @SuccessResponse("201", "Created") + public async uploadDocument( + @Request() request: ExpressRequest, + @UploadedFile() file: Express.Multer.File, + @FormField() tenantId: string, + @FormField() title?: string, + @FormField() description?: string, + @FormField() metadata?: string, + @FormField() tags?: string, + @FormField() linkedTaskIds?: string, + @FormField() linkedSubTaskIds?: string, + @FormField() linkedUserIds?: string + ): Promise { + if (!file) { + throw new ApplicationError("A document file is required", 400, "DOCUMENT_FILE_REQUIRED"); + } + + this.setStatus(201); + return this.uploadDocumentHandler.execute({ + tenantId, + title: normalizeOptionalString(title), + description: normalizeOptionalString(description), + metadata: parseMetadata(metadata), + tags: parseTags(tags), + linkedTaskIds: parseLinkedIds(linkedTaskIds), + linkedSubTaskIds: parseLinkedIds(linkedSubTaskIds), + linkedUserIds: parseLinkedIds(linkedUserIds), + uploadedByUserId: request.principal!.subject, + file: mapUploadedFile(file) + }); + } + + @Get() + @Security("bearerAuth", ["documents:read"]) + public async listDocuments( + @Request() request: ExpressRequest, + @Query() uploadedByUserId?: string, + @Query() targetType?: DocumentLinkTargetType, + @Query() targetId?: string + ): Promise { + return this.listDocumentsHandler.execute({ + tenantId: request.principal!.tenantId!, + uploadedByUserId, + targetType, + targetId + }); + } + + @Get("{documentId}") + @Security("bearerAuth", ["documents:read"]) + public async getDocumentById( + @Request() request: ExpressRequest, + @Path() documentId: string + ): Promise { + return this.getDocumentByIdHandler.execute({ + tenantId: request.principal!.tenantId!, + documentId + }); + } + + @Get("{documentId}/download") + @Security("bearerAuth", ["documents:read"]) + public async downloadDocument( + @Request() request: ExpressRequest, + @Path() documentId: string + ): Promise { + return this.downloadDocumentHandler.execute({ + tenantId: request.principal!.tenantId!, + documentId + }); + } + + @Patch("{documentId}") + @Security("bearerAuth", ["documents:update"]) + public async updateDocument( + @Path() documentId: string, + @Body() requestBody: UpdateDocumentRequestDto + ): Promise { + return this.updateDocumentHandler.execute({ + tenantId: requestBody.tenantId, + documentId, + title: normalizeOptionalString(requestBody.title), + description: requestBody.description === null + ? null + : normalizeOptionalString(requestBody.description), + metadata: parseMetadata(requestBody.metadata), + tags: parseTags(requestBody.tags), + linkedTaskIds: parseLinkedIds(requestBody.linkedTaskIds), + linkedSubTaskIds: parseLinkedIds(requestBody.linkedSubTaskIds), + linkedUserIds: parseLinkedIds(requestBody.linkedUserIds) + }); + } + + @Delete("{documentId}") + @Security("bearerAuth", ["documents:delete"]) + @SuccessResponse("204", "Deleted") + public async deleteDocument( + @Request() request: ExpressRequest, + @Path() documentId: string + ): Promise { + this.setStatus(204); + await this.deleteDocumentHandler.execute({ + tenantId: request.principal!.tenantId!, + documentId + }); + } +} + +function mapUploadedFile(file: Express.Multer.File): UploadDocumentFileDto { + return { + buffer: file.buffer, + originalFileName: file.originalname, + mimeType: file.mimetype || "application/octet-stream", + sizeBytes: file.size + }; +} diff --git a/src/modules/identity-access/application/commands/assign-roles/assign-roles.command.ts b/src/modules/identity-access/application/commands/assign-roles/assign-roles.command.ts new file mode 100644 index 0000000..ea6b80e --- /dev/null +++ b/src/modules/identity-access/application/commands/assign-roles/assign-roles.command.ts @@ -0,0 +1,5 @@ +export interface AssignRolesCommand { + tenantId: string; + userId: string; + roleCodes: string[]; +} diff --git a/src/modules/identity-access/application/commands/assign-roles/assign-roles.dto.ts b/src/modules/identity-access/application/commands/assign-roles/assign-roles.dto.ts new file mode 100644 index 0000000..7f4fc5a --- /dev/null +++ b/src/modules/identity-access/application/commands/assign-roles/assign-roles.dto.ts @@ -0,0 +1,3 @@ +export interface AssignRolesRequestDto { + roleCodes: string[]; +} diff --git a/src/modules/identity-access/application/commands/assign-roles/assign-roles.handler.ts b/src/modules/identity-access/application/commands/assign-roles/assign-roles.handler.ts new file mode 100644 index 0000000..43fb6db --- /dev/null +++ b/src/modules/identity-access/application/commands/assign-roles/assign-roles.handler.ts @@ -0,0 +1,42 @@ +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { RoleRepository } from "../../../domain/repositories/role.repository"; +import { UserRepository } from "../../../domain/repositories/user.repository"; +import { toUserResponse, UserResponseDto } from "../../mappers/user-response.mapper"; +import { AssignRolesCommand } from "./assign-roles.command"; + +export class AssignRolesHandler { + constructor( + private readonly userRepository: UserRepository, + private readonly roleRepository: RoleRepository, + private readonly clock: ClockPort + ) {} + + public async execute(command: AssignRolesCommand): Promise { + const user = await this.userRepository.findByIdForTenant(command.tenantId, command.userId); + + if (!user) { + throw new ApplicationError("User not found", 404, "USER_NOT_FOUND"); + } + + if (command.roleCodes.length === 0) { + throw new ApplicationError("At least one role is required", 422, "ROLE_REQUIRED"); + } + + const roles = await this.roleRepository.findByCodes(command.tenantId, command.roleCodes); + + if (roles.length !== command.roleCodes.length) { + throw new ApplicationError( + "One or more roles do not exist", + 422, + "ROLE_NOT_FOUND" + ); + } + + user.assignRoles(roles, this.clock.now()); + + await this.userRepository.save(user); + + return toUserResponse(user); + } +} diff --git a/src/modules/identity-access/application/commands/create-user/create-user.command.ts b/src/modules/identity-access/application/commands/create-user/create-user.command.ts new file mode 100644 index 0000000..cffa392 --- /dev/null +++ b/src/modules/identity-access/application/commands/create-user/create-user.command.ts @@ -0,0 +1,8 @@ +export interface CreateUserCommand { + tenantId: string; + email: string; + firstName: string; + lastName: string; + password: string; + roleCodes?: string[]; +} diff --git a/src/modules/identity-access/application/commands/create-user/create-user.dto.ts b/src/modules/identity-access/application/commands/create-user/create-user.dto.ts new file mode 100644 index 0000000..0ddb845 --- /dev/null +++ b/src/modules/identity-access/application/commands/create-user/create-user.dto.ts @@ -0,0 +1,12 @@ +import { UserResponseDto } from "../../mappers/user-response.mapper"; + +export class CreateUserRequestDto { + tenantId!: string; + email!: string; + firstName!: string; + lastName!: string; + password!: string; + roleCodes?: string[]; +} + +export { UserResponseDto }; diff --git a/src/modules/identity-access/application/commands/create-user/create-user.handler.ts b/src/modules/identity-access/application/commands/create-user/create-user.handler.ts new file mode 100644 index 0000000..33157fb --- /dev/null +++ b/src/modules/identity-access/application/commands/create-user/create-user.handler.ts @@ -0,0 +1,109 @@ +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { IdGeneratorPort } from "../../../../../shared/application/ports/id-generator.port"; +import { PasswordHasherPort } from "../../../../../shared/application/ports/password-hasher.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { TenantRepository } from "../../../../tenancy/domain/repositories/tenant.repository"; +import { UserAggregate } from "../../../domain/aggregates/user.aggregate"; +import { RoleRepository } from "../../../domain/repositories/role.repository"; +import { UserRepository } from "../../../domain/repositories/user.repository"; +import { toUserResponse, UserResponseDto } from "../../mappers/user-response.mapper"; +import { CreateUserCommand } from "./create-user.command"; +import { TenantSubscriptionRepository } from "../../../../billing/domain/repositories/tenant-subscription.repository"; +import { SubscriptionPlanRepository } from "../../../../billing/domain/repositories/subscription-plan.repository"; + +export class CreateUserHandler { + constructor( + private readonly userRepository: UserRepository, + private readonly roleRepository: RoleRepository, + private readonly tenantRepository: TenantRepository, + private readonly subscriptionRepository: TenantSubscriptionRepository, + private readonly planRepository: SubscriptionPlanRepository, + private readonly passwordHasher: PasswordHasherPort, + private readonly clock: ClockPort, + private readonly idGenerator: IdGeneratorPort + ) {} + + public async execute(command: CreateUserCommand): Promise { + if (command.password.length < 12) { + throw new ApplicationError( + "Password must be at least 12 characters long", + 422, + "WEAK_PASSWORD" + ); + } + + const tenant = await this.tenantRepository.findById(command.tenantId); + + if (!tenant) { + throw new ApplicationError("Tenant not found", 404, "TENANT_NOT_FOUND"); + } + + if (tenant.status !== "active") { + throw new ApplicationError("Tenant is inactive", 403, "TENANT_INACTIVE"); + } + + // Subscription Limit Check + const subscription = await this.subscriptionRepository.findByTenantId(command.tenantId); + if (!subscription) { + throw new ApplicationError("Subscription not found", 500, "SUBSCRIPTION_REQUIRED"); + } + + const plan = await this.planRepository.findById(subscription.planId); + if (!plan) { + throw new ApplicationError("Plan not found", 500, "BILLING_CONFIGURATION_ERROR"); + } + + if (plan.maxUsers !== -1) { + const currentUserCount = await this.userRepository.countByTenantId(command.tenantId); + if (currentUserCount >= plan.maxUsers) { + throw new ApplicationError( + `User limit reached for your ${plan.name} plan (${plan.maxUsers} users). Please upgrade to add more users.`, + 403, + "USER_LIMIT_REACHED" + ); + } + } + + const existingUser = await this.userRepository.findByEmail( + command.tenantId, + command.email + ); + + if (existingUser) { + throw new ApplicationError( + "A user with this email already exists in the tenant", + 409, + "USER_EMAIL_CONFLICT" + ); + } + + const roleCodes = + command.roleCodes && command.roleCodes.length > 0 + ? command.roleCodes + : ["tenant_user"]; + const roles = await this.roleRepository.findByCodes(command.tenantId, roleCodes); + + if (roles.length !== roleCodes.length) { + throw new ApplicationError( + "One or more roles do not exist", + 422, + "ROLE_NOT_FOUND" + ); + } + + const user = UserAggregate.register({ + id: this.idGenerator.generate(), + tenantId: command.tenantId, + email: command.email, + firstName: command.firstName, + lastName: command.lastName, + passwordHash: await this.passwordHasher.hash(command.password), + roles, + now: this.clock.now() + }); + + await this.userRepository.save(user); + + return toUserResponse(user); + } +} diff --git a/src/modules/identity-access/application/commands/invite-user/invite-user.command.ts b/src/modules/identity-access/application/commands/invite-user/invite-user.command.ts new file mode 100644 index 0000000..0dceed9 --- /dev/null +++ b/src/modules/identity-access/application/commands/invite-user/invite-user.command.ts @@ -0,0 +1,7 @@ +export interface InviteUserCommand { + tenantId: string; + email: string; + firstName: string; + lastName: string; + roleCodes?: string[]; +} diff --git a/src/modules/identity-access/application/commands/invite-user/invite-user.dto.ts b/src/modules/identity-access/application/commands/invite-user/invite-user.dto.ts new file mode 100644 index 0000000..c4d47a1 --- /dev/null +++ b/src/modules/identity-access/application/commands/invite-user/invite-user.dto.ts @@ -0,0 +1,13 @@ +export interface InviteUserRequestDto { + /** + * @format uuid + */ + tenantId: string; + /** + * @format email + */ + email: string; + firstName: string; + lastName: string; + roleCodes?: string[]; +} diff --git a/src/modules/identity-access/application/commands/invite-user/invite-user.handler.ts b/src/modules/identity-access/application/commands/invite-user/invite-user.handler.ts new file mode 100644 index 0000000..0dfa5d1 --- /dev/null +++ b/src/modules/identity-access/application/commands/invite-user/invite-user.handler.ts @@ -0,0 +1,110 @@ +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { IdGeneratorPort } from "../../../../../shared/application/ports/id-generator.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { TenantRepository } from "../../../../tenancy/domain/repositories/tenant.repository"; +import { UserAggregate } from "../../../domain/aggregates/user.aggregate"; +import { RoleRepository } from "../../../domain/repositories/role.repository"; +import { UserRepository } from "../../../domain/repositories/user.repository"; +import { EmailPort } from "../../ports/email.port"; +import { toUserResponse, UserResponseDto } from "../../mappers/user-response.mapper"; +import { InviteUserCommand } from "./invite-user.command"; +import { TenantSubscriptionRepository } from "../../../../billing/domain/repositories/tenant-subscription.repository"; +import { SubscriptionPlanRepository } from "../../../../billing/domain/repositories/subscription-plan.repository"; + +export class InviteUserHandler { + constructor( + private readonly userRepository: UserRepository, + private readonly roleRepository: RoleRepository, + private readonly tenantRepository: TenantRepository, + private readonly subscriptionRepository: TenantSubscriptionRepository, + private readonly planRepository: SubscriptionPlanRepository, + private readonly emailSender: EmailPort, + private readonly clock: ClockPort, + private readonly idGenerator: IdGeneratorPort + ) {} + + public async execute(command: InviteUserCommand): Promise { + const tenant = await this.tenantRepository.findById(command.tenantId); + + if (!tenant) { + throw new ApplicationError("Tenant not found", 404, "TENANT_NOT_FOUND"); + } + + if (tenant.status !== "active") { + throw new ApplicationError("Tenant is inactive", 403, "TENANT_INACTIVE"); + } + + // Subscription Limit Check + const subscription = await this.subscriptionRepository.findByTenantId(command.tenantId); + if (!subscription) { + throw new ApplicationError("Subscription not found", 500, "SUBSCRIPTION_REQUIRED"); + } + + const plan = await this.planRepository.findById(subscription.planId); + if (!plan) { + throw new ApplicationError("Plan not found", 500, "BILLING_CONFIGURATION_ERROR"); + } + + if (plan.maxUsers !== -1) { + const currentUserCount = await this.userRepository.countByTenantId(command.tenantId); + if (currentUserCount >= plan.maxUsers) { + throw new ApplicationError( + `User limit reached for your ${plan.name} plan (${plan.maxUsers} users). Please upgrade to invite more users.`, + 403, + "USER_LIMIT_REACHED" + ); + } + } + + const existingUser = await this.userRepository.findByEmail( + command.tenantId, + command.email + ); + + if (existingUser) { + throw new ApplicationError( + "A user with this email already exists in the tenant", + 409, + "USER_EMAIL_CONFLICT" + ); + } + + const roleCodes = + command.roleCodes && command.roleCodes.length > 0 + ? command.roleCodes + : ["tenant_user"]; + const roles = await this.roleRepository.findByCodes(command.tenantId, roleCodes); + + if (roles.length !== roleCodes.length) { + throw new ApplicationError( + "One or more roles do not exist", + 422, + "ROLE_NOT_FOUND" + ); + } + + const user = UserAggregate.invite({ + id: this.idGenerator.generate(), + tenantId: command.tenantId, + email: command.email, + firstName: command.firstName, + lastName: command.lastName, + roles, + now: this.clock.now() + }); + + await this.userRepository.save(user); + + // Send invitation email + await this.emailSender.send({ + to: user.email, + subject: `Invitation to join ${tenant.name}`, + body: `Hello ${user.firstName},\n\nYou have been invited to join ${tenant.name} on Gem Track.\n\nPlease follow the link below to set up your account.` + }).catch((error) => { + // We log the error but don't fail the command as the user is already created + console.error("Failed to send invitation email", error); + }); + + return toUserResponse(user); + } +} diff --git a/src/modules/identity-access/application/commands/login/login.command.ts b/src/modules/identity-access/application/commands/login/login.command.ts new file mode 100644 index 0000000..987a2b5 --- /dev/null +++ b/src/modules/identity-access/application/commands/login/login.command.ts @@ -0,0 +1,5 @@ +export interface LoginCommand { + tenantId: string; + email: string; + password: string; +} diff --git a/src/modules/identity-access/application/commands/login/login.dto.ts b/src/modules/identity-access/application/commands/login/login.dto.ts new file mode 100644 index 0000000..ccf027a --- /dev/null +++ b/src/modules/identity-access/application/commands/login/login.dto.ts @@ -0,0 +1,5 @@ +export class LoginRequestDto { + tenantId!: string; + email!: string; + password!: string; +} diff --git a/src/modules/identity-access/application/commands/login/login.handler.ts b/src/modules/identity-access/application/commands/login/login.handler.ts new file mode 100644 index 0000000..e649b6f --- /dev/null +++ b/src/modules/identity-access/application/commands/login/login.handler.ts @@ -0,0 +1,52 @@ +import { PasswordHasherPort } from "../../../../../shared/application/ports/password-hasher.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { TenantRepository } from "../../../../tenancy/domain/repositories/tenant.repository"; +import { UserRepository } from "../../../domain/repositories/user.repository"; +import { toUserResponse } from "../../mappers/user-response.mapper"; +import { AccessTokenService } from "../../services/access-token.service"; +import { LoginCommand } from "./login.command"; +import { AuthResponseDto } from "../register-user/register-user.dto"; + +export class LoginHandler { + constructor( + private readonly userRepository: UserRepository, + private readonly tenantRepository: TenantRepository, + private readonly passwordHasher: PasswordHasherPort, + private readonly accessTokenService: AccessTokenService + ) {} + + public async execute(command: LoginCommand): Promise { + const tenant = await this.tenantRepository.findById(command.tenantId); + + if (!tenant || tenant.status !== "active") { + throw new ApplicationError("Invalid credentials", 401, "INVALID_CREDENTIALS"); + } + + const user = await this.userRepository.findByEmail( + command.tenantId, + command.email + ); + + if (!user) { + throw new ApplicationError("Invalid credentials", 401, "INVALID_CREDENTIALS"); + } + + const passwordMatches = await this.passwordHasher.compare( + command.password, + user.passwordHash + ); + + if (!passwordMatches) { + throw new ApplicationError("Invalid credentials", 401, "INVALID_CREDENTIALS"); + } + + if (user.status !== "active") { + throw new ApplicationError("User account is inactive", 403, "USER_INACTIVE"); + } + + return { + accessToken: this.accessTokenService.issueFor(user), + user: toUserResponse(user) + }; + } +} diff --git a/src/modules/identity-access/application/commands/register-user/register-user.command.ts b/src/modules/identity-access/application/commands/register-user/register-user.command.ts new file mode 100644 index 0000000..bd9447e --- /dev/null +++ b/src/modules/identity-access/application/commands/register-user/register-user.command.ts @@ -0,0 +1,7 @@ +export interface RegisterUserCommand { + tenantId: string; + email: string; + firstName: string; + lastName: string; + password: string; +} diff --git a/src/modules/identity-access/application/commands/register-user/register-user.dto.ts b/src/modules/identity-access/application/commands/register-user/register-user.dto.ts new file mode 100644 index 0000000..256098d --- /dev/null +++ b/src/modules/identity-access/application/commands/register-user/register-user.dto.ts @@ -0,0 +1,14 @@ +import { UserResponseDto } from "../../mappers/user-response.mapper"; + +export class RegisterUserRequestDto { + tenantId!: string; + email!: string; + firstName!: string; + lastName!: string; + password!: string; +} + +export class AuthResponseDto { + accessToken!: string; + user!: UserResponseDto; +} diff --git a/src/modules/identity-access/application/commands/register-user/register-user.handler.ts b/src/modules/identity-access/application/commands/register-user/register-user.handler.ts new file mode 100644 index 0000000..e9f2946 --- /dev/null +++ b/src/modules/identity-access/application/commands/register-user/register-user.handler.ts @@ -0,0 +1,85 @@ +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { IdGeneratorPort } from "../../../../../shared/application/ports/id-generator.port"; +import { PasswordHasherPort } from "../../../../../shared/application/ports/password-hasher.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { TenantRepository } from "../../../../tenancy/domain/repositories/tenant.repository"; +import { RoleRepository } from "../../../domain/repositories/role.repository"; +import { UserRepository } from "../../../domain/repositories/user.repository"; +import { UserAggregate } from "../../../domain/aggregates/user.aggregate"; +import { toUserResponse } from "../../mappers/user-response.mapper"; +import { RegisterUserCommand } from "./register-user.command"; +import { AuthResponseDto } from "./register-user.dto"; +import { AccessTokenService } from "../../services/access-token.service"; + +export class RegisterUserHandler { + constructor( + private readonly userRepository: UserRepository, + private readonly roleRepository: RoleRepository, + private readonly tenantRepository: TenantRepository, + private readonly passwordHasher: PasswordHasherPort, + private readonly clock: ClockPort, + private readonly idGenerator: IdGeneratorPort, + private readonly accessTokenService: AccessTokenService + ) {} + + public async execute(command: RegisterUserCommand): Promise { + if (command.password.length < 12) { + throw new ApplicationError( + "Password must be at least 12 characters long", + 422, + "WEAK_PASSWORD" + ); + } + + const tenant = await this.tenantRepository.findById(command.tenantId); + + if (!tenant) { + throw new ApplicationError("Tenant not found", 404, "TENANT_NOT_FOUND"); + } + + if (tenant.status !== "active") { + throw new ApplicationError("Tenant is inactive", 403, "TENANT_INACTIVE"); + } + + const existingUser = await this.userRepository.findByEmail( + command.tenantId, + command.email + ); + + if (existingUser) { + throw new ApplicationError( + "A user with this email already exists in the tenant", + 409, + "USER_EMAIL_CONFLICT" + ); + } + + const roles = await this.roleRepository.findByCodes(command.tenantId, ["tenant_admin"]); + + if (roles.length !== 1) { + throw new ApplicationError( + "The default tenant_admin role is not configured", + 500, + "DEFAULT_ROLE_MISSING" + ); + } + + const user = UserAggregate.register({ + id: this.idGenerator.generate(), + tenantId: command.tenantId, + email: command.email, + firstName: command.firstName, + lastName: command.lastName, + passwordHash: await this.passwordHasher.hash(command.password), + roles, + now: this.clock.now() + }); + + await this.userRepository.save(user); + + return { + accessToken: this.accessTokenService.issueFor(user), + user: toUserResponse(user) + }; + } +} diff --git a/src/modules/identity-access/application/commands/update-user-status/update-user-status.command.ts b/src/modules/identity-access/application/commands/update-user-status/update-user-status.command.ts new file mode 100644 index 0000000..688e2f4 --- /dev/null +++ b/src/modules/identity-access/application/commands/update-user-status/update-user-status.command.ts @@ -0,0 +1,7 @@ +import { UserStatus } from "../../../domain/aggregates/user.aggregate"; + +export interface UpdateUserStatusCommand { + tenantId: string; + userId: string; + status: UserStatus; +} diff --git a/src/modules/identity-access/application/commands/update-user-status/update-user-status.dto.ts b/src/modules/identity-access/application/commands/update-user-status/update-user-status.dto.ts new file mode 100644 index 0000000..4adace7 --- /dev/null +++ b/src/modules/identity-access/application/commands/update-user-status/update-user-status.dto.ts @@ -0,0 +1,5 @@ +import { UserStatus } from "../../../domain/aggregates/user.aggregate"; + +export interface UpdateUserStatusRequestDto { + status: UserStatus; +} diff --git a/src/modules/identity-access/application/commands/update-user-status/update-user-status.handler.ts b/src/modules/identity-access/application/commands/update-user-status/update-user-status.handler.ts new file mode 100644 index 0000000..f875682 --- /dev/null +++ b/src/modules/identity-access/application/commands/update-user-status/update-user-status.handler.ts @@ -0,0 +1,40 @@ +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { UserRepository } from "../../../domain/repositories/user.repository"; +import { toUserResponse, UserResponseDto } from "../../mappers/user-response.mapper"; +import { UpdateUserStatusCommand } from "./update-user-status.command"; + +export class UpdateUserStatusHandler { + constructor( + private readonly userRepository: UserRepository, + private readonly clock: ClockPort + ) {} + + public async execute(command: UpdateUserStatusCommand): Promise { + const user = await this.userRepository.findByIdForTenant(command.tenantId, command.userId); + + if (!user) { + throw new ApplicationError("User not found", 404, "USER_NOT_FOUND"); + } + + const now = this.clock.now(); + + switch (command.status) { + case "active": + user.activate(now); + break; + case "suspended": + user.deactivate(now); + break; + case "pending_invitation": + // This is usually set only during invitation, but if allowed: + throw new ApplicationError("Cannot manually set status to pending_invitation", 422, "INVALID_STATUS_TRANSITION"); + default: + throw new ApplicationError(`Invalid status: ${command.status}`, 422, "INVALID_STATUS"); + } + + await this.userRepository.save(user); + + return toUserResponse(user); + } +} diff --git a/src/modules/identity-access/application/mappers/user-response.mapper.ts b/src/modules/identity-access/application/mappers/user-response.mapper.ts new file mode 100644 index 0000000..0b0e0a1 --- /dev/null +++ b/src/modules/identity-access/application/mappers/user-response.mapper.ts @@ -0,0 +1,29 @@ +import { UserAggregate } from "../../domain/aggregates/user.aggregate"; + +export class UserResponseDto { + id!: string; + tenantId!: string; + email!: string; + firstName!: string; + lastName!: string; + status!: "active" | "suspended" | "pending_invitation"; + roles!: string[]; + permissions!: string[]; + createdAt!: string; + updatedAt!: string; +} + +export function toUserResponse(user: UserAggregate): UserResponseDto { + return { + id: user.id, + tenantId: user.tenantId, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + status: user.status, + roles: user.roles.map((role) => role.code), + permissions: user.permissions, + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt.toISOString() + }; +} diff --git a/src/modules/identity-access/application/ports/email.port.ts b/src/modules/identity-access/application/ports/email.port.ts new file mode 100644 index 0000000..9f4f870 --- /dev/null +++ b/src/modules/identity-access/application/ports/email.port.ts @@ -0,0 +1,9 @@ +export interface SendEmailProps { + to: string; + subject: string; + body: string; +} + +export interface EmailPort { + send(props: SendEmailProps): Promise; +} diff --git a/src/modules/identity-access/application/queries/get-current-user/get-current-user.handler.ts b/src/modules/identity-access/application/queries/get-current-user/get-current-user.handler.ts new file mode 100644 index 0000000..f82c05f --- /dev/null +++ b/src/modules/identity-access/application/queries/get-current-user/get-current-user.handler.ts @@ -0,0 +1,21 @@ +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { UserRepository } from "../../../domain/repositories/user.repository"; +import { toUserResponse, UserResponseDto } from "../../mappers/user-response.mapper"; +import { GetCurrentUserQuery } from "./get-current-user.query"; + +export class GetCurrentUserHandler { + constructor(private readonly userRepository: UserRepository) {} + + public async execute(query: GetCurrentUserQuery): Promise { + const user = await this.userRepository.findByIdForTenant( + query.tenantId, + query.userId + ); + + if (!user) { + throw new ApplicationError("User not found", 404, "USER_NOT_FOUND"); + } + + return toUserResponse(user); + } +} diff --git a/src/modules/identity-access/application/queries/get-current-user/get-current-user.query.ts b/src/modules/identity-access/application/queries/get-current-user/get-current-user.query.ts new file mode 100644 index 0000000..8da74f1 --- /dev/null +++ b/src/modules/identity-access/application/queries/get-current-user/get-current-user.query.ts @@ -0,0 +1,4 @@ +export interface GetCurrentUserQuery { + tenantId: string; + userId: string; +} diff --git a/src/modules/identity-access/application/queries/get-user-by-id/get-user-by-id.handler.ts b/src/modules/identity-access/application/queries/get-user-by-id/get-user-by-id.handler.ts new file mode 100644 index 0000000..7e42463 --- /dev/null +++ b/src/modules/identity-access/application/queries/get-user-by-id/get-user-by-id.handler.ts @@ -0,0 +1,21 @@ +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { UserRepository } from "../../../domain/repositories/user.repository"; +import { toUserResponse, UserResponseDto } from "../../mappers/user-response.mapper"; +import { GetUserByIdQuery } from "./get-user-by-id.query"; + +export class GetUserByIdHandler { + constructor(private readonly userRepository: UserRepository) {} + + public async execute(query: GetUserByIdQuery): Promise { + const user = await this.userRepository.findByIdForTenant( + query.tenantId, + query.userId + ); + + if (!user) { + throw new ApplicationError("User not found", 404, "USER_NOT_FOUND"); + } + + return toUserResponse(user); + } +} diff --git a/src/modules/identity-access/application/queries/get-user-by-id/get-user-by-id.query.ts b/src/modules/identity-access/application/queries/get-user-by-id/get-user-by-id.query.ts new file mode 100644 index 0000000..0f1ecd8 --- /dev/null +++ b/src/modules/identity-access/application/queries/get-user-by-id/get-user-by-id.query.ts @@ -0,0 +1,4 @@ +export interface GetUserByIdQuery { + tenantId: string; + userId: string; +} diff --git a/src/modules/identity-access/application/services/access-token.service.ts b/src/modules/identity-access/application/services/access-token.service.ts new file mode 100644 index 0000000..60016b3 --- /dev/null +++ b/src/modules/identity-access/application/services/access-token.service.ts @@ -0,0 +1,14 @@ +import { JwtService } from "../../../../shared/infrastructure/security/jwt.service"; +import { UserAggregate } from "../../domain/aggregates/user.aggregate"; +import { AuthPrincipalFactory } from "./auth-principal.factory"; + +export class AccessTokenService { + constructor( + private readonly jwtService: JwtService, + private readonly authPrincipalFactory: AuthPrincipalFactory + ) {} + + public issueFor(user: UserAggregate): string { + return this.jwtService.sign(this.authPrincipalFactory.create(user)); + } +} diff --git a/src/modules/identity-access/application/services/auth-principal.factory.ts b/src/modules/identity-access/application/services/auth-principal.factory.ts new file mode 100644 index 0000000..93d2230 --- /dev/null +++ b/src/modules/identity-access/application/services/auth-principal.factory.ts @@ -0,0 +1,14 @@ +import { AuthPrincipal } from "../../../../shared/domain/types/request-context"; +import { UserAggregate } from "../../domain/aggregates/user.aggregate"; + +export class AuthPrincipalFactory { + public create(user: UserAggregate): AuthPrincipal { + return { + subject: user.id, + email: user.email, + roles: user.roles.map((role) => role.code), + permissions: user.permissions, + tenantId: user.tenantId + }; + } +} diff --git a/src/modules/identity-access/domain/aggregates/user.aggregate.ts b/src/modules/identity-access/domain/aggregates/user.aggregate.ts new file mode 100644 index 0000000..39e348c --- /dev/null +++ b/src/modules/identity-access/domain/aggregates/user.aggregate.ts @@ -0,0 +1,198 @@ +import { AggregateRoot } from "../../../../shared/domain/base/aggregate-root"; +import { DomainError } from "../../../../shared/domain/errors/domain-error"; +import { RoleEntity, RolePrimitives } from "../entities/role.entity"; +import { Email } from "../value-objects/email.vo"; +import { PasswordHash } from "../value-objects/password-hash.vo"; + +export type UserStatus = "active" | "suspended" | "pending_invitation"; + +export interface UserPrimitives { + id: string; + tenantId: string; + email: string; + firstName: string; + lastName: string; + passwordHash: string; + status: UserStatus; + roles: RolePrimitives[]; + createdAt: Date; + updatedAt: Date; +} + +interface RegisterUserProps { + id: string; + tenantId: string; + email: string; + firstName: string; + lastName: string; + passwordHash: string; + roles: RoleEntity[]; + now: Date; +} + +interface InviteUserProps { + id: string; + tenantId: string; + email: string; + firstName: string; + lastName: string; + roles: RoleEntity[]; + now: Date; +} + +export class UserAggregate extends AggregateRoot { + private readonly rolesState: RoleEntity[]; + private constructor(private readonly state: UserPrimitives) { + super(state.id); + this.rolesState = state.roles.map((role) => new RoleEntity(role)); + } + + public static register(props: RegisterUserProps): UserAggregate { + const firstName = props.firstName.trim(); + const lastName = props.lastName.trim(); + + if (firstName.length < 2) { + throw new DomainError("First name must be at least 2 characters long"); + } + + if (lastName.length < 2) { + throw new DomainError("Last name must be at least 2 characters long"); + } + + if (!props.tenantId.trim()) { + throw new DomainError("Tenant id is required"); + } + + const aggregate = new UserAggregate({ + id: props.id, + tenantId: props.tenantId.trim(), + email: Email.create(props.email).value, + firstName, + lastName, + passwordHash: PasswordHash.create(props.passwordHash).value, + status: "active", + roles: props.roles.map((role) => role.toPrimitives()), + createdAt: props.now, + updatedAt: props.now + }); + + return aggregate; + } + + public static invite(props: InviteUserProps): UserAggregate { + const firstName = props.firstName.trim(); + const lastName = props.lastName.trim(); + + if (firstName.length < 2) { + throw new DomainError("First name must be at least 2 characters long"); + } + + if (lastName.length < 2) { + throw new DomainError("Last name must be at least 2 characters long"); + } + + if (!props.tenantId.trim()) { + throw new DomainError("Tenant id is required"); + } + + return new UserAggregate({ + id: props.id, + tenantId: props.tenantId.trim(), + email: Email.create(props.email).value, + firstName, + lastName, + passwordHash: "", // No password yet for invited users + status: "pending_invitation", + roles: props.roles.map((role) => role.toPrimitives()), + createdAt: props.now, + updatedAt: props.now + }); + } + + public static rehydrate(primitives: UserPrimitives): UserAggregate { + return new UserAggregate({ + ...primitives, + email: Email.create(primitives.email).value, + passwordHash: PasswordHash.create(primitives.passwordHash).value + }); + } + + public assignRoles(roles: RoleEntity[], now: Date): void { + if (roles.length === 0) { + throw new DomainError("At least one role is required"); + } + + this.rolesState.splice(0, this.rolesState.length, ...roles); + this.state.roles = roles.map((role) => role.toPrimitives()); + this.state.updatedAt = now; + } + + public activate(now: Date): void { + if (this.state.status === "active") { + return; + } + + this.state.status = "active"; + this.state.updatedAt = now; + } + + public deactivate(now: Date): void { + if (this.state.status === "suspended") { + return; + } + + this.state.status = "suspended"; + this.state.updatedAt = now; + } + + public hasPermission(permissionCode: string): boolean { + return this.permissions.includes(permissionCode); + } + + public get tenantId(): string { + return this.state.tenantId; + } + + public get email(): string { + return this.state.email; + } + + public get firstName(): string { + return this.state.firstName; + } + + public get lastName(): string { + return this.state.lastName; + } + + public get passwordHash(): string { + return this.state.passwordHash; + } + + public get status(): UserStatus { + return this.state.status; + } + + public get roles(): RoleEntity[] { + return [...this.rolesState]; + } + + public get permissions(): string[] { + return [...new Set(this.rolesState.flatMap((role) => role.permissions.map((permission) => permission.code)))]; + } + + public get createdAt(): Date { + return this.state.createdAt; + } + + public get updatedAt(): Date { + return this.state.updatedAt; + } + + public toPrimitives(): UserPrimitives { + return { + ...this.state, + roles: this.rolesState.map((role) => role.toPrimitives()) + }; + } +} diff --git a/src/modules/identity-access/domain/entities/permission.entity.ts b/src/modules/identity-access/domain/entities/permission.entity.ts new file mode 100644 index 0000000..f8fc478 --- /dev/null +++ b/src/modules/identity-access/domain/entities/permission.entity.ts @@ -0,0 +1,30 @@ +import { Entity } from "../../../../shared/domain/base/entity"; + +export interface PermissionPrimitives { + id: string; + code: string; + name: string; + description?: string; +} + +export class PermissionEntity extends Entity { + constructor(private readonly state: PermissionPrimitives) { + super(state.id); + } + + public get code(): string { + return this.state.code; + } + + public get name(): string { + return this.state.name; + } + + public get description(): string | undefined { + return this.state.description; + } + + public toPrimitives(): PermissionPrimitives { + return { ...this.state }; + } +} diff --git a/src/modules/identity-access/domain/entities/role-permission.entity.ts b/src/modules/identity-access/domain/entities/role-permission.entity.ts new file mode 100644 index 0000000..3fcb441 --- /dev/null +++ b/src/modules/identity-access/domain/entities/role-permission.entity.ts @@ -0,0 +1,17 @@ +import { Entity } from "../../../../shared/domain/base/entity"; + +export interface RolePermissionPrimitives { + id: string; + roleId: string; + permissionId: string; +} + +export class RolePermissionEntity extends Entity { + constructor(private readonly state: RolePermissionPrimitives) { + super(state.id); + } + + public toPrimitives(): RolePermissionPrimitives { + return { ...this.state }; + } +} diff --git a/src/modules/identity-access/domain/entities/role.entity.ts b/src/modules/identity-access/domain/entities/role.entity.ts new file mode 100644 index 0000000..76a0494 --- /dev/null +++ b/src/modules/identity-access/domain/entities/role.entity.ts @@ -0,0 +1,46 @@ +import { Entity } from "../../../../shared/domain/base/entity"; +import { PermissionEntity, PermissionPrimitives } from "./permission.entity"; + +export interface RolePrimitives { + id: string; + code: string; + name: string; + description?: string; + permissions: PermissionPrimitives[]; +} + +export class RoleEntity extends Entity { + private readonly permissionsState: PermissionEntity[]; + + constructor(private readonly state: RolePrimitives) { + super(state.id); + this.permissionsState = state.permissions.map( + (permission) => new PermissionEntity(permission) + ); + } + + public get code(): string { + return this.state.code; + } + + public get name(): string { + return this.state.name; + } + + public get description(): string | undefined { + return this.state.description; + } + + public get permissions(): PermissionEntity[] { + return [...this.permissionsState]; + } + + public toPrimitives(): RolePrimitives { + return { + ...this.state, + permissions: this.permissionsState.map((permission) => + permission.toPrimitives() + ) + }; + } +} diff --git a/src/modules/identity-access/domain/entities/user-role.entity.ts b/src/modules/identity-access/domain/entities/user-role.entity.ts new file mode 100644 index 0000000..1c49b5f --- /dev/null +++ b/src/modules/identity-access/domain/entities/user-role.entity.ts @@ -0,0 +1,18 @@ +import { Entity } from "../../../../shared/domain/base/entity"; + +export interface UserRolePrimitives { + id: string; + userId: string; + roleId: string; + assignedAt: Date; +} + +export class UserRoleEntity extends Entity { + constructor(private readonly state: UserRolePrimitives) { + super(state.id); + } + + public toPrimitives(): UserRolePrimitives { + return { ...this.state }; + } +} diff --git a/src/modules/identity-access/domain/repositories/role.repository.ts b/src/modules/identity-access/domain/repositories/role.repository.ts new file mode 100644 index 0000000..e90b7d5 --- /dev/null +++ b/src/modules/identity-access/domain/repositories/role.repository.ts @@ -0,0 +1,5 @@ +import { RoleEntity } from "../entities/role.entity"; + +export interface RoleRepository { + findByCodes(tenantId: string, codes: string[]): Promise; +} diff --git a/src/modules/identity-access/domain/repositories/user.repository.ts b/src/modules/identity-access/domain/repositories/user.repository.ts new file mode 100644 index 0000000..e87eb4a --- /dev/null +++ b/src/modules/identity-access/domain/repositories/user.repository.ts @@ -0,0 +1,8 @@ +import { Repository } from "../../../../shared/domain/base/repository"; +import { UserAggregate } from "../aggregates/user.aggregate"; + +export interface UserRepository extends Repository { + findByEmail(tenantId: string, email: string): Promise; + findByIdForTenant(tenantId: string, id: string): Promise; + countByTenantId(tenantId: string): Promise; +} diff --git a/src/modules/identity-access/domain/value-objects/email.vo.ts b/src/modules/identity-access/domain/value-objects/email.vo.ts new file mode 100644 index 0000000..7b5a389 --- /dev/null +++ b/src/modules/identity-access/domain/value-objects/email.vo.ts @@ -0,0 +1,26 @@ +import { ValueObject } from "../../../../shared/domain/base/value-object"; +import { DomainError } from "../../../../shared/domain/errors/domain-error"; + +interface EmailProps { + value: string; +} + +export class Email extends ValueObject { + private constructor(props: EmailProps) { + super(props); + } + + public static create(value: string): Email { + const normalized = value.trim().toLowerCase(); + + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)) { + throw new DomainError("A valid email address is required"); + } + + return new Email({ value: normalized }); + } + + public get value(): string { + return this.props.value; + } +} diff --git a/src/modules/identity-access/domain/value-objects/password-hash.vo.ts b/src/modules/identity-access/domain/value-objects/password-hash.vo.ts new file mode 100644 index 0000000..b2c196f --- /dev/null +++ b/src/modules/identity-access/domain/value-objects/password-hash.vo.ts @@ -0,0 +1,26 @@ +import { ValueObject } from "../../../../shared/domain/base/value-object"; +import { DomainError } from "../../../../shared/domain/errors/domain-error"; + +interface PasswordHashProps { + value: string; +} + +export class PasswordHash extends ValueObject { + private constructor(props: PasswordHashProps) { + super(props); + } + + public static create(value: string): PasswordHash { + const normalized = value.trim(); + + if (normalized.length < 20) { + throw new DomainError("Password hash is invalid"); + } + + return new PasswordHash({ value: normalized }); + } + + public get value(): string { + return this.props.value; + } +} diff --git a/src/modules/identity-access/infrastructure/persistence/typeorm/entities/permission.orm-entity.ts b/src/modules/identity-access/infrastructure/persistence/typeorm/entities/permission.orm-entity.ts new file mode 100644 index 0000000..0504671 --- /dev/null +++ b/src/modules/identity-access/infrastructure/persistence/typeorm/entities/permission.orm-entity.ts @@ -0,0 +1,28 @@ +import { Column, Entity, Index, OneToMany, PrimaryColumn } from "typeorm"; + +import { RolePermissionOrmEntity } from "./role-permission.orm-entity"; + +@Entity({ name: "permissions" }) +@Index(["tenantId", "code"], { unique: true }) +export class PermissionOrmEntity { + @PrimaryColumn("uuid") + id!: string; + + @Column({ type: "uuid", name: "tenant_id" }) + tenantId!: string; + + @Column({ type: "varchar", length: 120 }) + code!: string; + + @Column({ type: "varchar", length: 120 }) + name!: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + description?: string; + + @OneToMany( + () => RolePermissionOrmEntity, + (rolePermission) => rolePermission.permission + ) + roleAssignments!: RolePermissionOrmEntity[]; +} diff --git a/src/modules/identity-access/infrastructure/persistence/typeorm/entities/role-permission.orm-entity.ts b/src/modules/identity-access/infrastructure/persistence/typeorm/entities/role-permission.orm-entity.ts new file mode 100644 index 0000000..701cc62 --- /dev/null +++ b/src/modules/identity-access/infrastructure/persistence/typeorm/entities/role-permission.orm-entity.ts @@ -0,0 +1,33 @@ +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm"; + +import { PermissionOrmEntity } from "./permission.orm-entity"; +import { RoleOrmEntity } from "./role.orm-entity"; + +@Entity({ name: "role_permissions" }) +@Index(["tenantId", "roleId", "permissionId"], { unique: true }) +export class RolePermissionOrmEntity { + @PrimaryColumn({ type: "varchar", length: 100 }) + id!: string; + + @Column({ type: "uuid", name: "tenant_id" }) + tenantId!: string; + + @Column({ type: "uuid", name: "role_id" }) + roleId!: string; + + @Column({ type: "uuid", name: "permission_id" }) + permissionId!: string; + + @ManyToOne(() => RoleOrmEntity, (role) => role.permissionAssignments, { + onDelete: "CASCADE" + }) + @JoinColumn({ name: "role_id" }) + role!: RoleOrmEntity; + + @ManyToOne(() => PermissionOrmEntity, (permission) => permission.roleAssignments, { + eager: true, + onDelete: "CASCADE" + }) + @JoinColumn({ name: "permission_id" }) + permission!: PermissionOrmEntity; +} diff --git a/src/modules/identity-access/infrastructure/persistence/typeorm/entities/role.orm-entity.ts b/src/modules/identity-access/infrastructure/persistence/typeorm/entities/role.orm-entity.ts new file mode 100644 index 0000000..7a05852 --- /dev/null +++ b/src/modules/identity-access/infrastructure/persistence/typeorm/entities/role.orm-entity.ts @@ -0,0 +1,39 @@ +import { + Column, + Entity, + Index, + OneToMany, + PrimaryColumn +} from "typeorm"; + +import { RolePermissionOrmEntity } from "./role-permission.orm-entity"; +import { UserRoleOrmEntity } from "./user-role.orm-entity"; + +@Entity({ name: "roles" }) +@Index(["tenantId", "code"], { unique: true }) +export class RoleOrmEntity { + @PrimaryColumn("uuid") + id!: string; + + @Column({ type: "uuid", name: "tenant_id" }) + tenantId!: string; + + @Column({ type: "varchar", length: 100 }) + code!: string; + + @Column({ type: "varchar", length: 120 }) + name!: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + description?: string; + + @OneToMany(() => UserRoleOrmEntity, (userRole) => userRole.role) + userAssignments!: UserRoleOrmEntity[]; + + @OneToMany( + () => RolePermissionOrmEntity, + (rolePermission) => rolePermission.role, + { eager: true, cascade: true } + ) + permissionAssignments!: RolePermissionOrmEntity[]; +} diff --git a/src/modules/identity-access/infrastructure/persistence/typeorm/entities/user-role.orm-entity.ts b/src/modules/identity-access/infrastructure/persistence/typeorm/entities/user-role.orm-entity.ts new file mode 100644 index 0000000..2c4b0ee --- /dev/null +++ b/src/modules/identity-access/infrastructure/persistence/typeorm/entities/user-role.orm-entity.ts @@ -0,0 +1,36 @@ +import { Column, Entity, Index, ManyToOne, JoinColumn, PrimaryColumn } from "typeorm"; + +import { RoleOrmEntity } from "./role.orm-entity"; +import { UserOrmEntity } from "./user.orm-entity"; + +@Entity({ name: "user_roles" }) +@Index(["tenantId", "userId", "roleId"], { unique: true }) +export class UserRoleOrmEntity { + @PrimaryColumn({ type: "varchar", length: 100 }) + id!: string; + + @Column({ type: "uuid", name: "tenant_id" }) + tenantId!: string; + + @Column({ type: "uuid", name: "user_id" }) + userId!: string; + + @Column({ type: "uuid", name: "role_id" }) + roleId!: string; + + @Column({ type: "timestamptz", name: "assigned_at" }) + assignedAt!: Date; + + @ManyToOne(() => UserOrmEntity, (user) => user.roleAssignments, { + onDelete: "CASCADE" + }) + @JoinColumn({ name: "user_id" }) + user!: UserOrmEntity; + + @ManyToOne(() => RoleOrmEntity, (role) => role.userAssignments, { + eager: true, + onDelete: "CASCADE" + }) + @JoinColumn({ name: "role_id" }) + role!: RoleOrmEntity; +} diff --git a/src/modules/identity-access/infrastructure/persistence/typeorm/entities/user.orm-entity.ts b/src/modules/identity-access/infrastructure/persistence/typeorm/entities/user.orm-entity.ts new file mode 100644 index 0000000..a383045 --- /dev/null +++ b/src/modules/identity-access/infrastructure/persistence/typeorm/entities/user.orm-entity.ts @@ -0,0 +1,46 @@ +import { + Column, + Entity, + Index, + OneToMany, + PrimaryColumn +} from "typeorm"; + +import { UserRoleOrmEntity } from "./user-role.orm-entity"; + +@Entity({ name: "users" }) +@Index(["tenantId", "email"], { unique: true }) +export class UserOrmEntity { + @PrimaryColumn("uuid") + id!: string; + + @Column({ type: "uuid", name: "tenant_id" }) + tenantId!: string; + + @Column({ type: "varchar", length: 255 }) + email!: string; + + @Column({ type: "varchar", name: "first_name", length: 100 }) + firstName!: string; + + @Column({ type: "varchar", name: "last_name", length: 100 }) + lastName!: string; + + @Column({ type: "varchar", name: "password_hash", length: 255 }) + passwordHash!: string; + + @Column({ type: "varchar", length: 20 }) + status!: string; + + @Column({ type: "timestamptz", name: "created_at" }) + createdAt!: Date; + + @Column({ type: "timestamptz", name: "updated_at" }) + updatedAt!: Date; + + @OneToMany(() => UserRoleOrmEntity, (userRole) => userRole.user, { + cascade: true, + eager: true + }) + roleAssignments!: UserRoleOrmEntity[]; +} diff --git a/src/modules/identity-access/infrastructure/persistence/typeorm/mappers/user-persistence.mapper.ts b/src/modules/identity-access/infrastructure/persistence/typeorm/mappers/user-persistence.mapper.ts new file mode 100644 index 0000000..bf8ba5d --- /dev/null +++ b/src/modules/identity-access/infrastructure/persistence/typeorm/mappers/user-persistence.mapper.ts @@ -0,0 +1,100 @@ +import { PermissionPrimitives } from "../../../../domain/entities/permission.entity"; +import { RolePrimitives } from "../../../../domain/entities/role.entity"; +import { UserAggregate } from "../../../../domain/aggregates/user.aggregate"; +import { PermissionOrmEntity } from "../entities/permission.orm-entity"; +import { RoleOrmEntity } from "../entities/role.orm-entity"; +import { RolePermissionOrmEntity } from "../entities/role-permission.orm-entity"; +import { UserOrmEntity } from "../entities/user.orm-entity"; +import { UserRoleOrmEntity } from "../entities/user-role.orm-entity"; + +function toPermissionPrimitives(entity: PermissionOrmEntity): PermissionPrimitives { + return { + id: entity.id, + code: entity.code, + name: entity.name, + description: entity.description + }; +} + +function toRolePrimitives(entity: RoleOrmEntity): RolePrimitives { + return { + id: entity.id, + code: entity.code, + name: entity.name, + description: entity.description, + permissions: (entity.permissionAssignments ?? []).map((assignment) => + toPermissionPrimitives(assignment.permission) + ) + }; +} + +export function toUserDomain(entity: UserOrmEntity): UserAggregate { + return UserAggregate.rehydrate({ + id: entity.id, + tenantId: entity.tenantId, + email: entity.email, + firstName: entity.firstName, + lastName: entity.lastName, + passwordHash: entity.passwordHash, + status: entity.status === "suspended" ? "suspended" : "active", + roles: (entity.roleAssignments ?? []) + .filter((assignment) => assignment.tenantId === entity.tenantId) + .map((assignment) => toRolePrimitives(assignment.role)), + createdAt: entity.createdAt, + updatedAt: entity.updatedAt + }); +} + +export function toUserPersistence(user: UserAggregate): UserOrmEntity { + const primitives = user.toPrimitives(); + const entity = new UserOrmEntity(); + + entity.id = primitives.id; + entity.tenantId = primitives.tenantId; + entity.email = primitives.email; + entity.firstName = primitives.firstName; + entity.lastName = primitives.lastName; + entity.passwordHash = primitives.passwordHash; + entity.status = primitives.status; + entity.createdAt = primitives.createdAt; + entity.updatedAt = primitives.updatedAt; + entity.roleAssignments = primitives.roles.map((role) => { + const roleEntity = new RoleOrmEntity(); + roleEntity.id = role.id; + roleEntity.tenantId = primitives.tenantId; + roleEntity.code = role.code; + roleEntity.name = role.name; + roleEntity.description = role.description; + roleEntity.permissionAssignments = role.permissions.map((permission) => { + const permissionEntity = new PermissionOrmEntity(); + permissionEntity.id = permission.id; + permissionEntity.tenantId = primitives.tenantId; + permissionEntity.code = permission.code; + permissionEntity.name = permission.name; + permissionEntity.description = permission.description; + + const rolePermission = new RolePermissionOrmEntity(); + rolePermission.id = `${role.id}:${permission.id}`; + rolePermission.tenantId = primitives.tenantId; + rolePermission.roleId = role.id; + rolePermission.permissionId = permission.id; + rolePermission.permission = permissionEntity; + rolePermission.role = roleEntity; + + return rolePermission; + }); + + const assignment = new UserRoleOrmEntity(); + assignment.id = `${primitives.id}:${role.id}`; + assignment.tenantId = primitives.tenantId; + assignment.userId = primitives.id; + assignment.roleId = role.id; + assignment.assignedAt = primitives.updatedAt; + assignment.role = roleEntity; + assignment.user = entity; + + return assignment; + }); + + return entity; +} diff --git a/src/modules/identity-access/infrastructure/persistence/typeorm/repositories/typeorm-role.repository.ts b/src/modules/identity-access/infrastructure/persistence/typeorm/repositories/typeorm-role.repository.ts new file mode 100644 index 0000000..4219940 --- /dev/null +++ b/src/modules/identity-access/infrastructure/persistence/typeorm/repositories/typeorm-role.repository.ts @@ -0,0 +1,44 @@ +import { In } from "typeorm"; + +import { TenantDataSourceResolver } from "../../../../../../shared/infrastructure/persistence/typeorm/tenant-data-source.resolver"; +import { RoleEntity } from "../../../../domain/entities/role.entity"; +import { RoleRepository } from "../../../../domain/repositories/role.repository"; +import { RoleOrmEntity } from "../entities/role.orm-entity"; + +export class TypeOrmRoleRepository implements RoleRepository { + constructor( + private readonly tenantDataSourceResolver: TenantDataSourceResolver + ) {} + + public async findByCodes(tenantId: string, codes: string[]): Promise { + const dataSource = await this.tenantDataSourceResolver.getPlatformDataSource(); + const repository = dataSource.getRepository(RoleOrmEntity); + const entities = await repository.find({ + where: { + tenantId, + code: In(codes) + }, + relations: { + permissionAssignments: { + permission: true + } + } + }); + + return entities.map( + (entity) => + new RoleEntity({ + id: entity.id, + code: entity.code, + name: entity.name, + description: entity.description, + permissions: (entity.permissionAssignments ?? []).map((assignment) => ({ + id: assignment.permission.id, + code: assignment.permission.code, + name: assignment.permission.name, + description: assignment.permission.description + })) + }) + ); + } +} diff --git a/src/modules/identity-access/infrastructure/persistence/typeorm/repositories/typeorm-user.repository.ts b/src/modules/identity-access/infrastructure/persistence/typeorm/repositories/typeorm-user.repository.ts new file mode 100644 index 0000000..e64329a --- /dev/null +++ b/src/modules/identity-access/infrastructure/persistence/typeorm/repositories/typeorm-user.repository.ts @@ -0,0 +1,100 @@ +import { TenantDataSourceResolver } from "../../../../../../shared/infrastructure/persistence/typeorm/tenant-data-source.resolver"; +import { UserAggregate } from "../../../../domain/aggregates/user.aggregate"; +import { UserRepository } from "../../../../domain/repositories/user.repository"; +import { UserOrmEntity } from "../entities/user.orm-entity"; +import { toUserDomain, toUserPersistence } from "../mappers/user-persistence.mapper"; + +function requireTenantId(tenantId: string): string { + const normalized = tenantId.trim(); + + if (!normalized) { + throw new Error("Tenant id is required for tenant-scoped queries"); + } + + return normalized; +} + +export class TypeOrmUserRepository implements UserRepository { + constructor( + private readonly tenantDataSourceResolver: TenantDataSourceResolver + ) {} + + public async findById(id: string): Promise { + const repository = await this.getRepository(); + const entity = await repository.findOne({ + where: { id }, + relations: { + roleAssignments: { + role: { + permissionAssignments: { + permission: true + } + } + } + } + }); + + return entity ? toUserDomain(entity) : null; + } + + public async findByIdForTenant( + tenantId: string, + id: string + ): Promise { + const repository = await this.getRepository(); + const entity = await repository.findOne({ + where: { id, tenantId: requireTenantId(tenantId) }, + relations: { + roleAssignments: { + role: { + permissionAssignments: { + permission: true + } + } + } + } + }); + + return entity ? toUserDomain(entity) : null; + } + + public async countByTenantId(tenantId: string): Promise { + const repository = await this.getRepository(); + return repository.countBy({ tenantId: requireTenantId(tenantId) }); + } + + public async findByEmail( + tenantId: string, + email: string + ): Promise { + const repository = await this.getRepository(); + const entity = await repository.findOne({ + where: { + tenantId: requireTenantId(tenantId), + email: email.trim().toLowerCase() + }, + relations: { + roleAssignments: { + role: { + permissionAssignments: { + permission: true + } + } + } + } + }); + + return entity ? toUserDomain(entity) : null; + } + + public async save(aggregate: UserAggregate): Promise { + const repository = await this.getRepository(); + const entity = toUserPersistence(aggregate); + await repository.save(entity); + } + + private async getRepository() { + const dataSource = await this.tenantDataSourceResolver.getPlatformDataSource(); + return dataSource.getRepository(UserOrmEntity); + } +} diff --git a/src/modules/identity-access/infrastructure/services/console-email-sender.ts b/src/modules/identity-access/infrastructure/services/console-email-sender.ts new file mode 100644 index 0000000..76cd951 --- /dev/null +++ b/src/modules/identity-access/infrastructure/services/console-email-sender.ts @@ -0,0 +1,20 @@ +import { EmailPort, SendEmailProps } from "../../application/ports/email.port"; +import { Logger } from "../../../../shared/infrastructure/logging/logger"; + +export class ConsoleEmailSender implements EmailPort { + private readonly logger = new Logger("ConsoleEmailSender"); + + public async send(props: SendEmailProps): Promise { + this.logger.info("Sending email...", { + to: props.to, + subject: props.subject + }); + + console.log("------------------------------------------"); + console.log(`TO: ${props.to}`); + console.log(`SUBJECT: ${props.subject}`); + console.log("BODY:"); + console.log(props.body); + console.log("------------------------------------------"); + } +} diff --git a/src/modules/identity-access/infrastructure/services/identity-access-seeder.ts b/src/modules/identity-access/infrastructure/services/identity-access-seeder.ts new file mode 100644 index 0000000..6c5737f --- /dev/null +++ b/src/modules/identity-access/infrastructure/services/identity-access-seeder.ts @@ -0,0 +1,184 @@ +import { randomUUID } from "crypto"; + +import { TenantProvisionerPort } from "../../../tenancy/application/ports/tenant-provisioner.port"; +import { TenantDataSourceResolver } from "../../../../shared/infrastructure/persistence/typeorm/tenant-data-source.resolver"; +import { PermissionOrmEntity } from "../persistence/typeorm/entities/permission.orm-entity"; +import { RoleOrmEntity } from "../persistence/typeorm/entities/role.orm-entity"; +import { RolePermissionOrmEntity } from "../persistence/typeorm/entities/role-permission.orm-entity"; + +const DEFAULT_PERMISSIONS = [ + { + code: "users:create", + name: "Create users", + description: "Create users within a tenant" + }, + { + code: "users:read", + name: "Read users", + description: "Read users within a tenant" + }, + { + code: "users:manage-roles", + name: "Manage user roles", + description: "Assign and change user roles" + }, + { + code: "users:manage", + name: "Manage users", + description: "Activate, deactivate and manage user status" + }, + { + code: "tasks:create", + name: "Create tasks", + description: "Create tasks within a tenant" + }, + { + code: "tasks:read", + name: "Read tasks", + description: "Read tasks within a tenant" + }, + { + code: "tasks:update", + name: "Update tasks", + description: "Update tasks and subtasks within a tenant" + }, + { + code: "tasks:delete", + name: "Delete tasks", + description: "Delete tasks within a tenant" + }, + { + code: "documents:create", + name: "Upload documents", + description: "Upload and link documents" + }, + { + code: "documents:read", + name: "Read documents", + description: "View and download documents" + }, + { + code: "documents:update", + name: "Update documents", + description: "Update document metadata and links" + }, + { + code: "documents:delete", + name: "Delete documents", + description: "Delete documents from the system" + } +] as const; + +const DEFAULT_ROLES = [ + { + code: "tenant_admin", + name: "Tenant administrator", + description: "Administrative role for tenant operations", + permissionCodes: [ + "users:create", + "users:read", + "users:manage-roles", + "users:manage", + "tasks:create", + "tasks:read", + "tasks:update", + "tasks:delete", + "documents:create", + "documents:read", + "documents:update", + "documents:delete" + ] + }, + { + code: "tenant_user", + name: "Tenant user", + description: "Standard tenant user", + permissionCodes: [ + "tasks:create", + "tasks:read", + "tasks:update", + "documents:create", + "documents:read", + "documents:update" + ] + } +] as const; + +export class IdentityAccessSeeder implements TenantProvisionerPort { + constructor( + private readonly tenantDataSourceResolver: TenantDataSourceResolver + ) {} + + public async provisionTenant(tenantId: string): Promise { + const dataSource = await this.tenantDataSourceResolver.getPlatformDataSource(); + const permissionRepository = dataSource.getRepository(PermissionOrmEntity); + const roleRepository = dataSource.getRepository(RoleOrmEntity); + const rolePermissionRepository = dataSource.getRepository(RolePermissionOrmEntity); + + for (const permission of DEFAULT_PERMISSIONS) { + const existing = await permissionRepository.findOneBy({ + tenantId, + code: permission.code + }); + + if (!existing) { + const entity = new PermissionOrmEntity(); + entity.id = randomUUID(); + entity.tenantId = tenantId; + entity.code = permission.code; + entity.name = permission.name; + entity.description = permission.description; + await permissionRepository.save(entity); + } + } + + const permissions = await permissionRepository.findBy({ tenantId }); + + for (const role of DEFAULT_ROLES) { + let existingRole = await roleRepository.findOne({ + where: { tenantId, code: role.code }, + relations: { + permissionAssignments: { + permission: true + } + } + }); + + if (!existingRole) { + existingRole = new RoleOrmEntity(); + existingRole.id = randomUUID(); + existingRole.tenantId = tenantId; + existingRole.code = role.code; + existingRole.name = role.name; + existingRole.description = role.description; + existingRole.permissionAssignments = []; + await roleRepository.save(existingRole); + } + + for (const permissionCode of role.permissionCodes) { + const permission = permissions.find((item) => item.code === permissionCode); + + if (!permission) { + continue; + } + + const existingAssignment = await rolePermissionRepository.findOneBy({ + tenantId, + roleId: existingRole.id, + permissionId: permission.id + }); + + if (!existingAssignment) { + const assignment = new RolePermissionOrmEntity(); + assignment.id = randomUUID(); + assignment.tenantId = tenantId; + assignment.roleId = existingRole.id; + assignment.permissionId = permission.id; + assignment.role = existingRole; + assignment.permission = permission; + await rolePermissionRepository.save(assignment); + } + } + } + } +} diff --git a/src/modules/identity-access/interfaces/http/controllers/auth.controller.ts b/src/modules/identity-access/interfaces/http/controllers/auth.controller.ts new file mode 100644 index 0000000..6f7eb7a --- /dev/null +++ b/src/modules/identity-access/interfaces/http/controllers/auth.controller.ts @@ -0,0 +1,49 @@ +import type { Request as ExpressRequest } from "express"; +import { Body, Controller, Get, Post, Request, Response, Route, Security, SuccessResponse, Tags } from "tsoa"; + +import { LoginRequestDto } from "../../../application/commands/login/login.dto"; +import { LoginHandler } from "../../../application/commands/login/login.handler"; +import { AuthResponseDto, RegisterUserRequestDto } from "../../../application/commands/register-user/register-user.dto"; +import { RegisterUserHandler } from "../../../application/commands/register-user/register-user.handler"; +import { GetCurrentUserHandler } from "../../../application/queries/get-current-user/get-current-user.handler"; +import { UserResponseDto } from "../../../application/mappers/user-response.mapper"; + +@Route("v1/auth") +@Tags("Auth") +export class AuthController extends Controller { + constructor( + private readonly registerUserHandler: RegisterUserHandler, + private readonly loginHandler: LoginHandler, + private readonly getCurrentUserHandler: GetCurrentUserHandler + ) { + super(); + } + + @Post("register") + @SuccessResponse("201", "Created") + public async register( + @Body() requestBody: RegisterUserRequestDto + ): Promise { + this.setStatus(201); + return this.registerUserHandler.execute(requestBody); + } + + @Post("login") + public async login( + @Body() requestBody: LoginRequestDto + ): Promise { + return this.loginHandler.execute(requestBody); + } + + @Get("me") + @Security("bearerAuth") + @Response("401", "Unauthorized") + public async me( + @Request() request: ExpressRequest + ): Promise { + return this.getCurrentUserHandler.execute({ + tenantId: request.principal!.tenantId!, + userId: request.principal!.subject + }); + } +} diff --git a/src/modules/identity-access/interfaces/http/controllers/users.controller.ts b/src/modules/identity-access/interfaces/http/controllers/users.controller.ts new file mode 100644 index 0000000..75e9ef6 --- /dev/null +++ b/src/modules/identity-access/interfaces/http/controllers/users.controller.ts @@ -0,0 +1,93 @@ +import type { Request as ExpressRequest } from "express"; +import { Body, Controller, Get, Path, Post, Request, Response, Route, Security, SuccessResponse, Tags } from "tsoa"; + +import { CreateUserRequestDto } from "../../../application/commands/create-user/create-user.dto"; +import { CreateUserHandler } from "../../../application/commands/create-user/create-user.handler"; +import { InviteUserRequestDto } from "../../../application/commands/invite-user/invite-user.dto"; +import { InviteUserHandler } from "../../../application/commands/invite-user/invite-user.handler"; +import { AssignRolesRequestDto } from "../../../application/commands/assign-roles/assign-roles.dto"; +import { AssignRolesHandler } from "../../../application/commands/assign-roles/assign-roles.handler"; +import { UpdateUserStatusRequestDto } from "../../../application/commands/update-user-status/update-user-status.dto"; +import { UpdateUserStatusHandler } from "../../../application/commands/update-user-status/update-user-status.handler"; +import { UserResponseDto } from "../../../application/mappers/user-response.mapper"; +import { GetUserByIdHandler } from "../../../application/queries/get-user-by-id/get-user-by-id.handler"; +import { Patch, Query } from "tsoa"; + +@Route("v1/users") +@Tags("Users") +export class UsersController extends Controller { + constructor( + private readonly createUserHandler: CreateUserHandler, + private readonly inviteUserHandler: InviteUserHandler, + private readonly assignRolesHandler: AssignRolesHandler, + private readonly updateUserStatusHandler: UpdateUserStatusHandler, + private readonly getUserByIdHandler: GetUserByIdHandler + ) { + super(); + } + + @Post() + @Security("bearerAuth", ["users:create"]) + @SuccessResponse("201", "Created") + public async createUser( + @Body() requestBody: CreateUserRequestDto + ): Promise { + this.setStatus(201); + return this.createUserHandler.execute(requestBody); + } + + @Get("{userId}") + @Security("bearerAuth", ["users:read"]) + @Response("404", "User not found") + public async getUserById( + @Request() request: ExpressRequest, + @Path() userId: string + ): Promise { + return this.getUserByIdHandler.execute({ + tenantId: request.principal!.tenantId!, + userId + }); + } + + @Post("invite") + @Security("bearerAuth", ["users:create"]) + @SuccessResponse("201", "Created") + public async inviteUser( + @Request() request: ExpressRequest, + @Body() requestBody: InviteUserRequestDto + ): Promise { + this.setStatus(201); + return this.inviteUserHandler.execute({ + ...requestBody, + tenantId: request.principal!.tenantId! + }); + } + + @Patch("{userId}/roles") + @Security("bearerAuth", ["users:manage-roles"]) + public async assignRoles( + @Request() request: ExpressRequest, + @Path() userId: string, + @Body() requestBody: AssignRolesRequestDto + ): Promise { + return this.assignRolesHandler.execute({ + tenantId: request.principal!.tenantId!, + userId, + roleCodes: requestBody.roleCodes + }); + } + + @Patch("{userId}/status") + @Security("bearerAuth", ["users:manage"]) + public async updateUserStatus( + @Request() request: ExpressRequest, + @Path() userId: string, + @Body() requestBody: UpdateUserStatusRequestDto + ): Promise { + return this.updateUserStatusHandler.execute({ + tenantId: request.principal!.tenantId!, + userId, + status: requestBody.status + }); + } +} diff --git a/src/modules/task-management/application/commands/add-subtask/add-subtask.command.ts b/src/modules/task-management/application/commands/add-subtask/add-subtask.command.ts new file mode 100644 index 0000000..8c5dff5 --- /dev/null +++ b/src/modules/task-management/application/commands/add-subtask/add-subtask.command.ts @@ -0,0 +1,8 @@ +export interface AddSubTaskCommand { + tenantId: string; + taskId: string; + title: string; + description?: string; + assignedUserId?: string; + dueDate?: string; +} diff --git a/src/modules/task-management/application/commands/add-subtask/add-subtask.dto.ts b/src/modules/task-management/application/commands/add-subtask/add-subtask.dto.ts new file mode 100644 index 0000000..4febe33 --- /dev/null +++ b/src/modules/task-management/application/commands/add-subtask/add-subtask.dto.ts @@ -0,0 +1,11 @@ +import { TaskResponseDto } from "../../mappers/task-response.mapper"; + +export class AddSubTaskRequestDto { + tenantId!: string; + title!: string; + description?: string; + assignedUserId?: string; + dueDate?: string; +} + +export { TaskResponseDto }; diff --git a/src/modules/task-management/application/commands/add-subtask/add-subtask.handler.ts b/src/modules/task-management/application/commands/add-subtask/add-subtask.handler.ts new file mode 100644 index 0000000..aa04d71 --- /dev/null +++ b/src/modules/task-management/application/commands/add-subtask/add-subtask.handler.ts @@ -0,0 +1,56 @@ +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { IdGeneratorPort } from "../../../../../shared/application/ports/id-generator.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { UserRepository } from "../../../../identity-access/domain/repositories/user.repository"; +import { TaskRepository } from "../../../domain/repositories/task.repository"; +import { parseOptionalDate } from "../../mappers/date-parser"; +import { toTaskResponse, TaskResponseDto } from "../../mappers/task-response.mapper"; +import { AddSubTaskCommand } from "./add-subtask.command"; + +export class AddSubTaskHandler { + constructor( + private readonly taskRepository: TaskRepository, + private readonly userRepository: UserRepository, + private readonly clock: ClockPort, + private readonly idGenerator: IdGeneratorPort + ) {} + + public async execute(command: AddSubTaskCommand): Promise { + const task = await this.taskRepository.findByIdForTenant( + command.tenantId, + command.taskId + ); + + if (!task) { + throw new ApplicationError("Task not found", 404, "TASK_NOT_FOUND"); + } + + if (command.assignedUserId) { + const user = await this.userRepository.findByIdForTenant( + command.tenantId, + command.assignedUserId + ); + + if (!user) { + throw new ApplicationError( + "Assigned user does not belong to the tenant", + 422, + "INVALID_ASSIGNEE" + ); + } + } + + task.addSubTask({ + id: this.idGenerator.generate(), + title: command.title, + description: command.description, + assignedUserId: command.assignedUserId, + dueDate: parseOptionalDate(command.dueDate, "dueDate"), + now: this.clock.now() + }); + + await this.taskRepository.save(task); + + return toTaskResponse(task); + } +} diff --git a/src/modules/task-management/application/commands/create-task/create-task.command.ts b/src/modules/task-management/application/commands/create-task/create-task.command.ts new file mode 100644 index 0000000..e0fee18 --- /dev/null +++ b/src/modules/task-management/application/commands/create-task/create-task.command.ts @@ -0,0 +1,10 @@ +import { TaskPriority } from "../../../domain/aggregates/task.aggregate"; + +export interface CreateTaskCommand { + tenantId: string; + title: string; + description?: string; + priority: TaskPriority; + dueDate?: string; + assignedUserId?: string; +} diff --git a/src/modules/task-management/application/commands/create-task/create-task.dto.ts b/src/modules/task-management/application/commands/create-task/create-task.dto.ts new file mode 100644 index 0000000..f4793af --- /dev/null +++ b/src/modules/task-management/application/commands/create-task/create-task.dto.ts @@ -0,0 +1,13 @@ +import { TaskPriority } from "../../../domain/aggregates/task.aggregate"; +import { TaskResponseDto } from "../../mappers/task-response.mapper"; + +export class CreateTaskRequestDto { + tenantId!: string; + title!: string; + description?: string; + priority!: TaskPriority; + dueDate?: string; + assignedUserId?: string; +} + +export { TaskResponseDto }; diff --git a/src/modules/task-management/application/commands/create-task/create-task.handler.ts b/src/modules/task-management/application/commands/create-task/create-task.handler.ts new file mode 100644 index 0000000..a823137 --- /dev/null +++ b/src/modules/task-management/application/commands/create-task/create-task.handler.ts @@ -0,0 +1,58 @@ +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { IdGeneratorPort } from "../../../../../shared/application/ports/id-generator.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { UserRepository } from "../../../../identity-access/domain/repositories/user.repository"; +import { TenantRepository } from "../../../../tenancy/domain/repositories/tenant.repository"; +import { TaskAggregate } from "../../../domain/aggregates/task.aggregate"; +import { TaskRepository } from "../../../domain/repositories/task.repository"; +import { parseOptionalDate } from "../../mappers/date-parser"; +import { toTaskResponse, TaskResponseDto } from "../../mappers/task-response.mapper"; +import { CreateTaskCommand } from "./create-task.command"; + +export class CreateTaskHandler { + constructor( + private readonly taskRepository: TaskRepository, + private readonly tenantRepository: TenantRepository, + private readonly userRepository: UserRepository, + private readonly clock: ClockPort, + private readonly idGenerator: IdGeneratorPort + ) {} + + public async execute(command: CreateTaskCommand): Promise { + const tenant = await this.tenantRepository.findById(command.tenantId); + + if (!tenant) { + throw new ApplicationError("Tenant not found", 404, "TENANT_NOT_FOUND"); + } + + if (command.assignedUserId) { + const user = await this.userRepository.findByIdForTenant( + command.tenantId, + command.assignedUserId + ); + + if (!user) { + throw new ApplicationError( + "Assigned user does not belong to the tenant", + 422, + "INVALID_ASSIGNEE" + ); + } + } + + const task = TaskAggregate.create({ + id: this.idGenerator.generate(), + tenantId: command.tenantId, + title: command.title, + description: command.description, + priority: command.priority, + dueDate: parseOptionalDate(command.dueDate, "dueDate"), + assignedUserId: command.assignedUserId, + now: this.clock.now() + }); + + await this.taskRepository.save(task); + + return toTaskResponse(task); + } +} diff --git a/src/modules/task-management/application/commands/delete-subtask/delete-subtask.command.ts b/src/modules/task-management/application/commands/delete-subtask/delete-subtask.command.ts new file mode 100644 index 0000000..e8d3227 --- /dev/null +++ b/src/modules/task-management/application/commands/delete-subtask/delete-subtask.command.ts @@ -0,0 +1,5 @@ +export interface DeleteSubTaskCommand { + tenantId: string; + taskId: string; + subTaskId: string; +} diff --git a/src/modules/task-management/application/commands/delete-subtask/delete-subtask.handler.ts b/src/modules/task-management/application/commands/delete-subtask/delete-subtask.handler.ts new file mode 100644 index 0000000..23cc650 --- /dev/null +++ b/src/modules/task-management/application/commands/delete-subtask/delete-subtask.handler.ts @@ -0,0 +1,25 @@ +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { TaskRepository } from "../../../domain/repositories/task.repository"; +import { DeleteSubTaskCommand } from "./delete-subtask.command"; + +export class DeleteSubTaskHandler { + constructor( + private readonly taskRepository: TaskRepository, + private readonly clock: ClockPort + ) {} + + public async execute(command: DeleteSubTaskCommand): Promise { + const task = await this.taskRepository.findByIdForTenant( + command.tenantId, + command.taskId + ); + + if (!task) { + throw new ApplicationError("Task not found", 404, "TASK_NOT_FOUND"); + } + + task.removeSubTask(command.subTaskId, this.clock.now()); + await this.taskRepository.save(task); + } +} diff --git a/src/modules/task-management/application/commands/delete-task/delete-task.command.ts b/src/modules/task-management/application/commands/delete-task/delete-task.command.ts new file mode 100644 index 0000000..839a47c --- /dev/null +++ b/src/modules/task-management/application/commands/delete-task/delete-task.command.ts @@ -0,0 +1,4 @@ +export interface DeleteTaskCommand { + tenantId: string; + taskId: string; +} diff --git a/src/modules/task-management/application/commands/delete-task/delete-task.handler.ts b/src/modules/task-management/application/commands/delete-task/delete-task.handler.ts new file mode 100644 index 0000000..2f7a8bc --- /dev/null +++ b/src/modules/task-management/application/commands/delete-task/delete-task.handler.ts @@ -0,0 +1,20 @@ +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { TaskRepository } from "../../../domain/repositories/task.repository"; +import { DeleteTaskCommand } from "./delete-task.command"; + +export class DeleteTaskHandler { + constructor(private readonly taskRepository: TaskRepository) {} + + public async execute(command: DeleteTaskCommand): Promise { + const task = await this.taskRepository.findByIdForTenant( + command.tenantId, + command.taskId + ); + + if (!task) { + throw new ApplicationError("Task not found", 404, "TASK_NOT_FOUND"); + } + + await this.taskRepository.deleteForTenant(command.tenantId, command.taskId); + } +} diff --git a/src/modules/task-management/application/commands/update-subtask/update-subtask.command.ts b/src/modules/task-management/application/commands/update-subtask/update-subtask.command.ts new file mode 100644 index 0000000..1367aa0 --- /dev/null +++ b/src/modules/task-management/application/commands/update-subtask/update-subtask.command.ts @@ -0,0 +1,12 @@ +import { SubTaskStatus } from "../../../domain/aggregates/task.aggregate"; + +export interface UpdateSubTaskCommand { + tenantId: string; + taskId: string; + subTaskId: string; + title?: string; + description?: string | null; + status?: SubTaskStatus; + assignedUserId?: string | null; + dueDate?: string | null; +} diff --git a/src/modules/task-management/application/commands/update-subtask/update-subtask.dto.ts b/src/modules/task-management/application/commands/update-subtask/update-subtask.dto.ts new file mode 100644 index 0000000..56670f9 --- /dev/null +++ b/src/modules/task-management/application/commands/update-subtask/update-subtask.dto.ts @@ -0,0 +1,13 @@ +import { SubTaskStatus } from "../../../domain/aggregates/task.aggregate"; +import { TaskResponseDto } from "../../mappers/task-response.mapper"; + +export class UpdateSubTaskRequestDto { + tenantId!: string; + title?: string; + description?: string | null; + status?: SubTaskStatus; + assignedUserId?: string | null; + dueDate?: string | null; +} + +export { TaskResponseDto }; diff --git a/src/modules/task-management/application/commands/update-subtask/update-subtask.handler.ts b/src/modules/task-management/application/commands/update-subtask/update-subtask.handler.ts new file mode 100644 index 0000000..1a08937 --- /dev/null +++ b/src/modules/task-management/application/commands/update-subtask/update-subtask.handler.ts @@ -0,0 +1,55 @@ +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { UserRepository } from "../../../../identity-access/domain/repositories/user.repository"; +import { TaskRepository } from "../../../domain/repositories/task.repository"; +import { parseNullableDate } from "../../mappers/date-parser"; +import { toTaskResponse, TaskResponseDto } from "../../mappers/task-response.mapper"; +import { UpdateSubTaskCommand } from "./update-subtask.command"; + +export class UpdateSubTaskHandler { + constructor( + private readonly taskRepository: TaskRepository, + private readonly userRepository: UserRepository, + private readonly clock: ClockPort + ) {} + + public async execute(command: UpdateSubTaskCommand): Promise { + const task = await this.taskRepository.findByIdForTenant( + command.tenantId, + command.taskId + ); + + if (!task) { + throw new ApplicationError("Task not found", 404, "TASK_NOT_FOUND"); + } + + if (command.assignedUserId) { + const user = await this.userRepository.findByIdForTenant( + command.tenantId, + command.assignedUserId + ); + + if (!user) { + throw new ApplicationError( + "Assigned user does not belong to the tenant", + 422, + "INVALID_ASSIGNEE" + ); + } + } + + task.updateSubTask({ + subTaskId: command.subTaskId, + title: command.title, + description: command.description, + status: command.status, + assignedUserId: command.assignedUserId, + dueDate: parseNullableDate(command.dueDate, "dueDate"), + now: this.clock.now() + }); + + await this.taskRepository.save(task); + + return toTaskResponse(task); + } +} diff --git a/src/modules/task-management/application/commands/update-task/update-task.command.ts b/src/modules/task-management/application/commands/update-task/update-task.command.ts new file mode 100644 index 0000000..0a30c34 --- /dev/null +++ b/src/modules/task-management/application/commands/update-task/update-task.command.ts @@ -0,0 +1,12 @@ +import { TaskPriority, TaskStatus } from "../../../domain/aggregates/task.aggregate"; + +export interface UpdateTaskCommand { + tenantId: string; + taskId: string; + title?: string; + description?: string | null; + status?: TaskStatus; + priority?: TaskPriority; + dueDate?: string | null; + assignedUserId?: string | null; +} diff --git a/src/modules/task-management/application/commands/update-task/update-task.dto.ts b/src/modules/task-management/application/commands/update-task/update-task.dto.ts new file mode 100644 index 0000000..6bd5fcb --- /dev/null +++ b/src/modules/task-management/application/commands/update-task/update-task.dto.ts @@ -0,0 +1,14 @@ +import { TaskPriority, TaskStatus } from "../../../domain/aggregates/task.aggregate"; +import { TaskResponseDto } from "../../mappers/task-response.mapper"; + +export class UpdateTaskRequestDto { + tenantId!: string; + title?: string; + description?: string | null; + status?: TaskStatus; + priority?: TaskPriority; + dueDate?: string | null; + assignedUserId?: string | null; +} + +export { TaskResponseDto }; diff --git a/src/modules/task-management/application/commands/update-task/update-task.handler.ts b/src/modules/task-management/application/commands/update-task/update-task.handler.ts new file mode 100644 index 0000000..aa50012 --- /dev/null +++ b/src/modules/task-management/application/commands/update-task/update-task.handler.ts @@ -0,0 +1,55 @@ +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { UserRepository } from "../../../../identity-access/domain/repositories/user.repository"; +import { TaskRepository } from "../../../domain/repositories/task.repository"; +import { parseNullableDate } from "../../mappers/date-parser"; +import { toTaskResponse, TaskResponseDto } from "../../mappers/task-response.mapper"; +import { UpdateTaskCommand } from "./update-task.command"; + +export class UpdateTaskHandler { + constructor( + private readonly taskRepository: TaskRepository, + private readonly userRepository: UserRepository, + private readonly clock: ClockPort + ) {} + + public async execute(command: UpdateTaskCommand): Promise { + const task = await this.taskRepository.findByIdForTenant( + command.tenantId, + command.taskId + ); + + if (!task) { + throw new ApplicationError("Task not found", 404, "TASK_NOT_FOUND"); + } + + if (command.assignedUserId) { + const user = await this.userRepository.findByIdForTenant( + command.tenantId, + command.assignedUserId + ); + + if (!user) { + throw new ApplicationError( + "Assigned user does not belong to the tenant", + 422, + "INVALID_ASSIGNEE" + ); + } + } + + task.updateDetails({ + title: command.title, + description: command.description, + status: command.status, + priority: command.priority, + dueDate: parseNullableDate(command.dueDate, "dueDate"), + assignedUserId: command.assignedUserId, + now: this.clock.now() + }); + + await this.taskRepository.save(task); + + return toTaskResponse(task); + } +} diff --git a/src/modules/task-management/application/mappers/date-parser.ts b/src/modules/task-management/application/mappers/date-parser.ts new file mode 100644 index 0000000..9d134ef --- /dev/null +++ b/src/modules/task-management/application/mappers/date-parser.ts @@ -0,0 +1,33 @@ +import { ApplicationError } from "../../../../shared/domain/errors/application-error"; + +export function parseOptionalDate( + input: string | undefined | null, + fieldName: string +): Date | undefined { + if (input === undefined || input === null || input === "") { + return undefined; + } + + const parsed = new Date(input); + + if (Number.isNaN(parsed.getTime())) { + throw new ApplicationError(`${fieldName} must be a valid ISO date`, 422, "INVALID_DATE"); + } + + return parsed; +} + +export function parseNullableDate( + input: string | undefined | null, + fieldName: string +): Date | null | undefined { + if (input === null) { + return null; + } + + if (input === undefined || input === "") { + return undefined; + } + + return parseOptionalDate(input, fieldName) ?? undefined; +} diff --git a/src/modules/task-management/application/mappers/task-response.mapper.ts b/src/modules/task-management/application/mappers/task-response.mapper.ts new file mode 100644 index 0000000..165ecb0 --- /dev/null +++ b/src/modules/task-management/application/mappers/task-response.mapper.ts @@ -0,0 +1,62 @@ +import { + TaskAggregate, + TaskPriority, + TaskStatus, + SubTaskStatus +} from "../../domain/aggregates/task.aggregate"; + +export class SubTaskResponseDto { + id!: string; + title!: string; + description?: string; + status!: SubTaskStatus; + assignedUserId?: string; + dueDate?: string; + completedAt?: string; + createdAt!: string; + updatedAt!: string; +} + +export class TaskResponseDto { + id!: string; + tenantId!: string; + title!: string; + description?: string; + status!: TaskStatus; + priority!: TaskPriority; + dueDate?: string; + assignedUserId?: string; + completedAt?: string; + createdAt!: string; + updatedAt!: string; + subtasks!: SubTaskResponseDto[]; +} + +export function toTaskResponse(task: TaskAggregate): TaskResponseDto { + const primitives = task.toPrimitives(); + + return { + id: primitives.id, + tenantId: primitives.tenantId, + title: primitives.title, + description: primitives.description, + status: primitives.status, + priority: primitives.priority, + dueDate: primitives.dueDate?.toISOString(), + assignedUserId: primitives.assignedUserId, + completedAt: primitives.completedAt?.toISOString(), + createdAt: primitives.createdAt.toISOString(), + updatedAt: primitives.updatedAt.toISOString(), + subtasks: primitives.subtasks.map((subtask) => ({ + id: subtask.id, + title: subtask.title, + description: subtask.description, + status: subtask.status, + assignedUserId: subtask.assignedUserId, + dueDate: subtask.dueDate?.toISOString(), + completedAt: subtask.completedAt?.toISOString(), + createdAt: subtask.createdAt.toISOString(), + updatedAt: subtask.updatedAt.toISOString() + })) + }; +} diff --git a/src/modules/task-management/application/queries/get-task-by-id/get-task-by-id.handler.ts b/src/modules/task-management/application/queries/get-task-by-id/get-task-by-id.handler.ts new file mode 100644 index 0000000..4a03e8e --- /dev/null +++ b/src/modules/task-management/application/queries/get-task-by-id/get-task-by-id.handler.ts @@ -0,0 +1,21 @@ +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { TaskRepository } from "../../../domain/repositories/task.repository"; +import { toTaskResponse, TaskResponseDto } from "../../mappers/task-response.mapper"; +import { GetTaskByIdQuery } from "./get-task-by-id.query"; + +export class GetTaskByIdHandler { + constructor(private readonly taskRepository: TaskRepository) {} + + public async execute(query: GetTaskByIdQuery): Promise { + const task = await this.taskRepository.findByIdForTenant( + query.tenantId, + query.taskId + ); + + if (!task) { + throw new ApplicationError("Task not found", 404, "TASK_NOT_FOUND"); + } + + return toTaskResponse(task); + } +} diff --git a/src/modules/task-management/application/queries/get-task-by-id/get-task-by-id.query.ts b/src/modules/task-management/application/queries/get-task-by-id/get-task-by-id.query.ts new file mode 100644 index 0000000..1e53fc6 --- /dev/null +++ b/src/modules/task-management/application/queries/get-task-by-id/get-task-by-id.query.ts @@ -0,0 +1,4 @@ +export interface GetTaskByIdQuery { + tenantId: string; + taskId: string; +} diff --git a/src/modules/task-management/application/queries/list-tasks/list-tasks.handler.ts b/src/modules/task-management/application/queries/list-tasks/list-tasks.handler.ts new file mode 100644 index 0000000..a5e9dee --- /dev/null +++ b/src/modules/task-management/application/queries/list-tasks/list-tasks.handler.ts @@ -0,0 +1,16 @@ +import { TaskRepository } from "../../../domain/repositories/task.repository"; +import { toTaskResponse, TaskResponseDto } from "../../mappers/task-response.mapper"; +import { ListTasksQuery } from "./list-tasks.query"; + +export class ListTasksHandler { + constructor(private readonly taskRepository: TaskRepository) {} + + public async execute(query: ListTasksQuery): Promise { + const tasks = await this.taskRepository.listForTenant(query.tenantId, { + status: query.status, + assignedUserId: query.assignedUserId + }); + + return tasks.map(toTaskResponse); + } +} diff --git a/src/modules/task-management/application/queries/list-tasks/list-tasks.query.ts b/src/modules/task-management/application/queries/list-tasks/list-tasks.query.ts new file mode 100644 index 0000000..95dcf05 --- /dev/null +++ b/src/modules/task-management/application/queries/list-tasks/list-tasks.query.ts @@ -0,0 +1,7 @@ +import { TaskStatus } from "../../../domain/aggregates/task.aggregate"; + +export interface ListTasksQuery { + tenantId: string; + status?: TaskStatus; + assignedUserId?: string; +} diff --git a/src/modules/task-management/domain/aggregates/task.aggregate.ts b/src/modules/task-management/domain/aggregates/task.aggregate.ts new file mode 100644 index 0000000..235a736 --- /dev/null +++ b/src/modules/task-management/domain/aggregates/task.aggregate.ts @@ -0,0 +1,278 @@ +import { AggregateRoot } from "../../../../shared/domain/base/aggregate-root"; +import { DomainError } from "../../../../shared/domain/errors/domain-error"; +import { SubTaskEntity, SubTaskPrimitives } from "../entities/subtask.entity"; + +export type TaskStatus = "todo" | "in_progress" | "completed"; +export type TaskPriority = "low" | "medium" | "high" | "critical"; +export type SubTaskStatus = "todo" | "in_progress" | "completed"; + +export interface TaskPrimitives { + id: string; + tenantId: string; + title: string; + description?: string; + status: TaskStatus; + priority: TaskPriority; + dueDate?: Date; + assignedUserId?: string; + completedAt?: Date; + createdAt: Date; + updatedAt: Date; + subtasks: SubTaskPrimitives[]; +} + +interface CreateTaskProps { + id: string; + tenantId: string; + title: string; + description?: string; + priority: TaskPriority; + dueDate?: Date; + assignedUserId?: string; + now: Date; +} + +interface AddSubTaskProps { + id: string; + title: string; + description?: string; + assignedUserId?: string; + dueDate?: Date; + now: Date; +} + +interface UpdateSubTaskProps { + subTaskId: string; + title?: string; + description?: string | null; + status?: SubTaskStatus; + assignedUserId?: string | null; + dueDate?: Date | null; + now: Date; +} + +export class TaskAggregate extends AggregateRoot { + private readonly subtasksState: SubTaskEntity[]; + + private constructor(private readonly state: TaskPrimitives) { + super(state.id); + this.subtasksState = state.subtasks.map((subtask) => new SubTaskEntity(subtask)); + } + + public static create(props: CreateTaskProps): TaskAggregate { + const title = props.title.trim(); + + if (!props.tenantId.trim()) { + throw new DomainError("Tenant id is required"); + } + + if (title.length < 3) { + throw new DomainError("Task title must be at least 3 characters long"); + } + + return new TaskAggregate({ + id: props.id, + tenantId: props.tenantId.trim(), + title, + description: props.description?.trim() || undefined, + status: "todo", + priority: props.priority, + dueDate: props.dueDate, + assignedUserId: props.assignedUserId?.trim() || undefined, + completedAt: undefined, + createdAt: props.now, + updatedAt: props.now, + subtasks: [] + }); + } + + public static rehydrate(primitives: TaskPrimitives): TaskAggregate { + return new TaskAggregate({ + ...primitives, + title: primitives.title.trim(), + description: primitives.description?.trim() || undefined, + assignedUserId: primitives.assignedUserId?.trim() || undefined + }); + } + + public updateDetails(props: { + title?: string; + description?: string | null; + status?: TaskStatus; + priority?: TaskPriority; + dueDate?: Date | null; + assignedUserId?: string | null; + now: Date; + }): void { + if (props.title !== undefined) { + const title = props.title.trim(); + + if (title.length < 3) { + throw new DomainError("Task title must be at least 3 characters long"); + } + + this.state.title = title; + } + + if (props.description !== undefined) { + this.state.description = props.description?.trim() || undefined; + } + + if (props.priority !== undefined) { + this.state.priority = props.priority; + } + + if (props.dueDate !== undefined) { + this.state.dueDate = props.dueDate ?? undefined; + } + + if (props.assignedUserId !== undefined) { + this.state.assignedUserId = props.assignedUserId?.trim() || undefined; + } + + if (props.status !== undefined) { + if (props.status === "completed") { + this.completeTask(props.now); + } else { + this.state.status = props.status; + this.state.completedAt = undefined; + if (props.status === "todo") { + this.subtasksState.forEach((subtask) => subtask.reopenIfCompleted(props.now)); + } + } + } + + this.state.updatedAt = props.now; + } + + public addSubTask(props: AddSubTaskProps): void { + const subTask = SubTaskEntity.create({ + id: props.id, + taskId: this.id, + tenantId: this.tenantId, + title: props.title, + description: props.description, + assignedUserId: props.assignedUserId, + dueDate: props.dueDate, + now: props.now + }); + + this.subtasksState.push(subTask); + this.syncTaskStatusFromSubtasks(props.now); + this.state.updatedAt = props.now; + } + + public updateSubTask(props: UpdateSubTaskProps): void { + const subTask = this.subtasksState.find((item) => item.id === props.subTaskId); + + if (!subTask) { + throw new DomainError("Subtask not found"); + } + + subTask.updateDetails({ + title: props.title, + description: props.description, + status: props.status, + assignedUserId: props.assignedUserId, + dueDate: props.dueDate, + now: props.now + }); + + this.syncTaskStatusFromSubtasks(props.now); + this.state.updatedAt = props.now; + } + + public removeSubTask(subTaskId: string, now: Date): void { + const index = this.subtasksState.findIndex((item) => item.id === subTaskId); + + if (index === -1) { + throw new DomainError("Subtask not found"); + } + + this.subtasksState.splice(index, 1); + this.syncTaskStatusFromSubtasks(now); + this.state.updatedAt = now; + } + + private completeTask(now: Date): void { + this.subtasksState.forEach((subtask) => subtask.complete(now)); + this.state.status = "completed"; + this.state.completedAt = now; + } + + private syncTaskStatusFromSubtasks(now: Date): void { + if (this.subtasksState.length === 0) { + if (this.state.status === "completed") { + this.state.status = "todo"; + this.state.completedAt = undefined; + } + + return; + } + + const allCompleted = this.subtasksState.every((subtask) => subtask.status === "completed"); + const anyStarted = this.subtasksState.some((subtask) => + subtask.status === "in_progress" || subtask.status === "completed" + ); + + if (allCompleted) { + this.state.status = "completed"; + this.state.completedAt = now; + return; + } + + this.state.completedAt = undefined; + this.state.status = anyStarted ? "in_progress" : "todo"; + } + + public get tenantId(): string { + return this.state.tenantId; + } + + public get title(): string { + return this.state.title; + } + + public get description(): string | undefined { + return this.state.description; + } + + public get status(): TaskStatus { + return this.state.status; + } + + public get priority(): TaskPriority { + return this.state.priority; + } + + public get dueDate(): Date | undefined { + return this.state.dueDate; + } + + public get assignedUserId(): string | undefined { + return this.state.assignedUserId; + } + + public get completedAt(): Date | undefined { + return this.state.completedAt; + } + + public get createdAt(): Date { + return this.state.createdAt; + } + + public get updatedAt(): Date { + return this.state.updatedAt; + } + + public get subtasks(): SubTaskEntity[] { + return [...this.subtasksState]; + } + + public toPrimitives(): TaskPrimitives { + return { + ...this.state, + subtasks: this.subtasksState.map((subtask) => subtask.toPrimitives()) + }; + } +} diff --git a/src/modules/task-management/domain/entities/subtask.entity.ts b/src/modules/task-management/domain/entities/subtask.entity.ts new file mode 100644 index 0000000..a9fd709 --- /dev/null +++ b/src/modules/task-management/domain/entities/subtask.entity.ts @@ -0,0 +1,120 @@ +import { Entity } from "../../../../shared/domain/base/entity"; +import { DomainError } from "../../../../shared/domain/errors/domain-error"; +import { SubTaskStatus } from "../aggregates/task.aggregate"; + +export interface SubTaskPrimitives { + id: string; + tenantId: string; + taskId: string; + title: string; + description?: string; + status: SubTaskStatus; + assignedUserId?: string; + dueDate?: Date; + completedAt?: Date; + createdAt: Date; + updatedAt: Date; +} + +interface CreateSubTaskProps { + id: string; + tenantId: string; + taskId: string; + title: string; + description?: string; + assignedUserId?: string; + dueDate?: Date; + now: Date; +} + +export class SubTaskEntity extends Entity { + constructor(private readonly state: SubTaskPrimitives) { + super(state.id); + } + + public static create(props: CreateSubTaskProps): SubTaskEntity { + const title = props.title.trim(); + + if (title.length < 3) { + throw new DomainError("Subtask title must be at least 3 characters long"); + } + + return new SubTaskEntity({ + id: props.id, + tenantId: props.tenantId, + taskId: props.taskId, + title, + description: props.description?.trim() || undefined, + status: "todo", + assignedUserId: props.assignedUserId?.trim() || undefined, + dueDate: props.dueDate, + completedAt: undefined, + createdAt: props.now, + updatedAt: props.now + }); + } + + public updateDetails(props: { + title?: string; + description?: string | null; + status?: SubTaskStatus; + assignedUserId?: string | null; + dueDate?: Date | null; + now: Date; + }): void { + if (props.title !== undefined) { + const title = props.title.trim(); + + if (title.length < 3) { + throw new DomainError("Subtask title must be at least 3 characters long"); + } + + this.state.title = title; + } + + if (props.description !== undefined) { + this.state.description = props.description?.trim() || undefined; + } + + if (props.assignedUserId !== undefined) { + this.state.assignedUserId = props.assignedUserId?.trim() || undefined; + } + + if (props.dueDate !== undefined) { + this.state.dueDate = props.dueDate ?? undefined; + } + + if (props.status !== undefined) { + if (props.status === "completed") { + this.complete(props.now); + } else { + this.state.status = props.status; + this.state.completedAt = undefined; + } + } + + this.state.updatedAt = props.now; + } + + public complete(now: Date): void { + this.state.status = "completed"; + this.state.completedAt = now; + this.state.updatedAt = now; + } + + public reopenIfCompleted(now: Date): void { + if (this.state.status === "completed") { + this.state.status = "todo"; + this.state.completedAt = undefined; + this.state.updatedAt = now; + } + } + + public get status(): SubTaskStatus { + return this.state.status; + } + + public toPrimitives(): SubTaskPrimitives { + return { ...this.state }; + } +} diff --git a/src/modules/task-management/domain/repositories/task.repository.ts b/src/modules/task-management/domain/repositories/task.repository.ts new file mode 100644 index 0000000..d590784 --- /dev/null +++ b/src/modules/task-management/domain/repositories/task.repository.ts @@ -0,0 +1,17 @@ +import { Repository } from "../../../../shared/domain/base/repository"; +import { TaskAggregate, TaskStatus } from "../aggregates/task.aggregate"; + +export interface ListTasksFilter { + status?: TaskStatus; + assignedUserId?: string; +} + +export interface TaskRepository extends Repository { + findByIdForTenant(tenantId: string, taskId: string): Promise; + findBySubTaskIdForTenant( + tenantId: string, + subTaskId: string + ): Promise; + listForTenant(tenantId: string, filter?: ListTasksFilter): Promise; + deleteForTenant(tenantId: string, taskId: string): Promise; +} diff --git a/src/modules/task-management/infrastructure/persistence/typeorm/entities/subtask.orm-entity.ts b/src/modules/task-management/infrastructure/persistence/typeorm/entities/subtask.orm-entity.ts new file mode 100644 index 0000000..8f5d317 --- /dev/null +++ b/src/modules/task-management/infrastructure/persistence/typeorm/entities/subtask.orm-entity.ts @@ -0,0 +1,46 @@ +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm"; + +import { TaskOrmEntity } from "./task.orm-entity"; + +@Entity({ name: "subtasks" }) +@Index(["tenantId", "taskId"]) +export class SubTaskOrmEntity { + @PrimaryColumn("uuid") + id!: string; + + @Column({ type: "uuid", name: "tenant_id" }) + tenantId!: string; + + @Column({ type: "uuid", name: "task_id" }) + taskId!: string; + + @Column({ type: "varchar", length: 200 }) + title!: string; + + @Column({ type: "text", nullable: true }) + description?: string; + + @Column({ type: "varchar", length: 20 }) + status!: string; + + @Column({ type: "uuid", name: "assigned_user_id", nullable: true }) + assignedUserId?: string; + + @Column({ type: "timestamptz", name: "due_date", nullable: true }) + dueDate?: Date; + + @Column({ type: "timestamptz", name: "completed_at", nullable: true }) + completedAt?: Date; + + @Column({ type: "timestamptz", name: "created_at" }) + createdAt!: Date; + + @Column({ type: "timestamptz", name: "updated_at" }) + updatedAt!: Date; + + @ManyToOne(() => TaskOrmEntity, (task) => task.subtasks, { + onDelete: "CASCADE" + }) + @JoinColumn({ name: "task_id" }) + task!: TaskOrmEntity; +} diff --git a/src/modules/task-management/infrastructure/persistence/typeorm/entities/task.orm-entity.ts b/src/modules/task-management/infrastructure/persistence/typeorm/entities/task.orm-entity.ts new file mode 100644 index 0000000..36832c5 --- /dev/null +++ b/src/modules/task-management/infrastructure/persistence/typeorm/entities/task.orm-entity.ts @@ -0,0 +1,48 @@ +import { Column, Entity, Index, OneToMany, PrimaryColumn } from "typeorm"; + +import { SubTaskOrmEntity } from "./subtask.orm-entity"; + +@Entity({ name: "tasks" }) +@Index(["tenantId", "status"]) +@Index(["tenantId", "assignedUserId"]) +export class TaskOrmEntity { + @PrimaryColumn("uuid") + id!: string; + + @Column({ type: "uuid", name: "tenant_id" }) + tenantId!: string; + + @Column({ type: "varchar", length: 200 }) + title!: string; + + @Column({ type: "text", nullable: true }) + description?: string; + + @Column({ type: "varchar", length: 20 }) + status!: string; + + @Column({ type: "varchar", length: 20 }) + priority!: string; + + @Column({ type: "uuid", name: "assigned_user_id", nullable: true }) + assignedUserId?: string; + + @Column({ type: "timestamptz", name: "due_date", nullable: true }) + dueDate?: Date; + + @Column({ type: "timestamptz", name: "completed_at", nullable: true }) + completedAt?: Date; + + @Column({ type: "timestamptz", name: "created_at" }) + createdAt!: Date; + + @Column({ type: "timestamptz", name: "updated_at" }) + updatedAt!: Date; + + @OneToMany(() => SubTaskOrmEntity, (subtask) => subtask.task, { + cascade: true, + eager: true, + orphanedRowAction: "delete" + }) + subtasks!: SubTaskOrmEntity[]; +} diff --git a/src/modules/task-management/infrastructure/persistence/typeorm/mappers/task-persistence.mapper.ts b/src/modules/task-management/infrastructure/persistence/typeorm/mappers/task-persistence.mapper.ts new file mode 100644 index 0000000..3e1a706 --- /dev/null +++ b/src/modules/task-management/infrastructure/persistence/typeorm/mappers/task-persistence.mapper.ts @@ -0,0 +1,82 @@ +import { TaskAggregate } from "../../../../domain/aggregates/task.aggregate"; +import { SubTaskOrmEntity } from "../entities/subtask.orm-entity"; +import { TaskOrmEntity } from "../entities/task.orm-entity"; + +export function toTaskDomain(entity: TaskOrmEntity): TaskAggregate { + return TaskAggregate.rehydrate({ + id: entity.id, + tenantId: entity.tenantId, + title: entity.title, + description: entity.description, + status: entity.status === "completed" + ? "completed" + : entity.status === "in_progress" + ? "in_progress" + : "todo", + priority: entity.priority === "critical" + ? "critical" + : entity.priority === "high" + ? "high" + : entity.priority === "low" + ? "low" + : "medium", + dueDate: entity.dueDate, + assignedUserId: entity.assignedUserId, + completedAt: entity.completedAt, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + subtasks: (entity.subtasks ?? []).map((subtask) => ({ + id: subtask.id, + tenantId: subtask.tenantId, + taskId: subtask.taskId, + title: subtask.title, + description: subtask.description, + status: subtask.status === "completed" + ? "completed" + : subtask.status === "in_progress" + ? "in_progress" + : "todo", + assignedUserId: subtask.assignedUserId, + dueDate: subtask.dueDate, + completedAt: subtask.completedAt, + createdAt: subtask.createdAt, + updatedAt: subtask.updatedAt + })) + }); +} + +export function toTaskPersistence(task: TaskAggregate): TaskOrmEntity { + const primitives = task.toPrimitives(); + const entity = new TaskOrmEntity(); + + entity.id = primitives.id; + entity.tenantId = primitives.tenantId; + entity.title = primitives.title; + entity.description = primitives.description; + entity.status = primitives.status; + entity.priority = primitives.priority; + entity.dueDate = primitives.dueDate; + entity.assignedUserId = primitives.assignedUserId; + entity.completedAt = primitives.completedAt; + entity.createdAt = primitives.createdAt; + entity.updatedAt = primitives.updatedAt; + entity.subtasks = primitives.subtasks.map((subtask) => { + const subtaskEntity = new SubTaskOrmEntity(); + subtaskEntity.id = subtask.id; + subtaskEntity.tenantId = subtask.tenantId; + subtaskEntity.taskId = subtask.taskId; + subtaskEntity.title = subtask.title; + subtaskEntity.description = subtask.description; + subtaskEntity.status = subtask.status; + subtaskEntity.assignedUserId = subtask.assignedUserId; + subtaskEntity.dueDate = subtask.dueDate; + subtaskEntity.completedAt = subtask.completedAt; + subtaskEntity.createdAt = subtask.createdAt; + subtaskEntity.updatedAt = subtask.updatedAt; + subtaskEntity.task = entity; + + return subtaskEntity; + }); + + return entity; +} diff --git a/src/modules/task-management/infrastructure/persistence/typeorm/repositories/typeorm-task.repository.ts b/src/modules/task-management/infrastructure/persistence/typeorm/repositories/typeorm-task.repository.ts new file mode 100644 index 0000000..73ddfea --- /dev/null +++ b/src/modules/task-management/infrastructure/persistence/typeorm/repositories/typeorm-task.repository.ts @@ -0,0 +1,96 @@ +import { FindOptionsWhere, Repository as TypeOrmRepository } from "typeorm"; + +import { TenantDataSourceResolver } from "../../../../../../shared/infrastructure/persistence/typeorm/tenant-data-source.resolver"; +import { TaskStatus } from "../../../../domain/aggregates/task.aggregate"; +import { ListTasksFilter, TaskRepository } from "../../../../domain/repositories/task.repository"; +import { TaskAggregate } from "../../../../domain/aggregates/task.aggregate"; +import { SubTaskOrmEntity } from "../entities/subtask.orm-entity"; +import { TaskOrmEntity } from "../entities/task.orm-entity"; +import { toTaskDomain, toTaskPersistence } from "../mappers/task-persistence.mapper"; + +export class TypeOrmTaskRepository implements TaskRepository { + constructor( + private readonly tenantDataSourceResolver: TenantDataSourceResolver + ) {} + + public async findById(id: string): Promise { + const repository = await this.getRepository(); + const entity = await repository.findOne({ + where: { id }, + relations: { subtasks: true } + }); + + return entity ? toTaskDomain(entity) : null; + } + + public async findByIdForTenant( + tenantId: string, + taskId: string + ): Promise { + const repository = await this.getRepository(); + const entity = await repository.findOne({ + where: { id: taskId, tenantId }, + relations: { subtasks: true } + }); + + return entity ? toTaskDomain(entity) : null; + } + + public async findBySubTaskIdForTenant( + tenantId: string, + subTaskId: string + ): Promise { + const repository = await this.getRepository(); + const entity = await repository + .createQueryBuilder("task") + .leftJoinAndSelect("task.subtasks", "subtask") + .where("task.tenant_id = :tenantId", { tenantId }) + .andWhere("subtask.id = :subTaskId", { subTaskId }) + .getOne(); + + return entity ? toTaskDomain(entity) : null; + } + + public async listForTenant( + tenantId: string, + filter?: ListTasksFilter + ): Promise { + const repository = await this.getRepository(); + const where: FindOptionsWhere = { + tenantId, + ...(filter?.status ? { status: filter.status } : {}), + ...(filter?.assignedUserId ? { assignedUserId: filter.assignedUserId } : {}) + }; + + const entities = await repository.find({ + where, + relations: { subtasks: true }, + order: { + dueDate: "ASC", + createdAt: "DESC" + } + }); + + return entities.map(toTaskDomain); + } + + public async save(aggregate: TaskAggregate): Promise { + const repository = await this.getRepository(); + await repository.save(toTaskPersistence(aggregate)); + } + + public async deleteForTenant(tenantId: string, taskId: string): Promise { + const repository = await this.getRepository(); + await repository.delete({ id: taskId, tenantId }); + } + + private async getRepository(): Promise> { + const dataSource = await this.tenantDataSourceResolver.getPlatformDataSource(); + return dataSource.getRepository(TaskOrmEntity); + } + + private async getSubTaskRepository(): Promise> { + const dataSource = await this.tenantDataSourceResolver.getPlatformDataSource(); + return dataSource.getRepository(SubTaskOrmEntity); + } +} diff --git a/src/modules/task-management/interfaces/http/controllers/tasks.controller.ts b/src/modules/task-management/interfaces/http/controllers/tasks.controller.ts new file mode 100644 index 0000000..ff77c61 --- /dev/null +++ b/src/modules/task-management/interfaces/http/controllers/tasks.controller.ts @@ -0,0 +1,139 @@ +import type { Request as ExpressRequest } from "express"; +import { Body, Controller, Delete, Get, Path, Patch, Post, Query, Request, Response, Route, Security, SuccessResponse, Tags } from "tsoa"; + +import { AddSubTaskRequestDto } from "../../../application/commands/add-subtask/add-subtask.dto"; +import { AddSubTaskHandler } from "../../../application/commands/add-subtask/add-subtask.handler"; +import { CreateTaskRequestDto } from "../../../application/commands/create-task/create-task.dto"; +import { CreateTaskHandler } from "../../../application/commands/create-task/create-task.handler"; +import { DeleteSubTaskHandler } from "../../../application/commands/delete-subtask/delete-subtask.handler"; +import { DeleteTaskHandler } from "../../../application/commands/delete-task/delete-task.handler"; +import { UpdateSubTaskRequestDto } from "../../../application/commands/update-subtask/update-subtask.dto"; +import { UpdateSubTaskHandler } from "../../../application/commands/update-subtask/update-subtask.handler"; +import { UpdateTaskRequestDto } from "../../../application/commands/update-task/update-task.dto"; +import { UpdateTaskHandler } from "../../../application/commands/update-task/update-task.handler"; +import { TaskStatus } from "../../../domain/aggregates/task.aggregate"; +import { TaskResponseDto } from "../../../application/mappers/task-response.mapper"; +import { GetTaskByIdHandler } from "../../../application/queries/get-task-by-id/get-task-by-id.handler"; +import { ListTasksHandler } from "../../../application/queries/list-tasks/list-tasks.handler"; + +@Route("v1/tasks") +@Tags("Tasks") +export class TasksController extends Controller { + constructor( + private readonly createTaskHandler: CreateTaskHandler, + private readonly updateTaskHandler: UpdateTaskHandler, + private readonly deleteTaskHandler: DeleteTaskHandler, + private readonly addSubTaskHandler: AddSubTaskHandler, + private readonly updateSubTaskHandler: UpdateSubTaskHandler, + private readonly deleteSubTaskHandler: DeleteSubTaskHandler, + private readonly getTaskByIdHandler: GetTaskByIdHandler, + private readonly listTasksHandler: ListTasksHandler + ) { + super(); + } + + @Post() + @Security("bearerAuth", ["tasks:create"]) + @SuccessResponse("201", "Created") + public async createTask( + @Body() requestBody: CreateTaskRequestDto + ): Promise { + this.setStatus(201); + return this.createTaskHandler.execute(requestBody); + } + + @Get() + @Security("bearerAuth", ["tasks:read"]) + public async listTasks( + @Request() request: ExpressRequest, + @Query() status?: TaskStatus, + @Query() assignedUserId?: string + ): Promise { + return this.listTasksHandler.execute({ + tenantId: request.principal!.tenantId!, + status, + assignedUserId + }); + } + + @Get("{taskId}") + @Security("bearerAuth", ["tasks:read"]) + @Response("404", "Task not found") + public async getTaskById( + @Request() request: ExpressRequest, + @Path() taskId: string + ): Promise { + return this.getTaskByIdHandler.execute({ + tenantId: request.principal!.tenantId!, + taskId + }); + } + + @Patch("{taskId}") + @Security("bearerAuth", ["tasks:update"]) + public async updateTask( + @Path() taskId: string, + @Body() requestBody: UpdateTaskRequestDto + ): Promise { + return this.updateTaskHandler.execute({ + ...requestBody, + taskId + }); + } + + @Delete("{taskId}") + @Security("bearerAuth", ["tasks:delete"]) + @SuccessResponse("204", "Deleted") + public async deleteTask( + @Request() request: ExpressRequest, + @Path() taskId: string + ): Promise { + this.setStatus(204); + await this.deleteTaskHandler.execute({ + tenantId: request.principal!.tenantId!, + taskId + }); + } + + @Post("{taskId}/subtasks") + @Security("bearerAuth", ["tasks:update"]) + public async addSubTask( + @Path() taskId: string, + @Body() requestBody: AddSubTaskRequestDto + ): Promise { + return this.addSubTaskHandler.execute({ + ...requestBody, + taskId + }); + } + + @Patch("{taskId}/subtasks/{subTaskId}") + @Security("bearerAuth", ["tasks:update"]) + public async updateSubTask( + @Path() taskId: string, + @Path() subTaskId: string, + @Body() requestBody: UpdateSubTaskRequestDto + ): Promise { + return this.updateSubTaskHandler.execute({ + ...requestBody, + taskId, + subTaskId + }); + } + + @Delete("{taskId}/subtasks/{subTaskId}") + @Security("bearerAuth", ["tasks:update"]) + @SuccessResponse("204", "Deleted") + public async deleteSubTask( + @Request() request: ExpressRequest, + @Path() taskId: string, + @Path() subTaskId: string + ): Promise { + this.setStatus(204); + await this.deleteSubTaskHandler.execute({ + tenantId: request.principal!.tenantId!, + taskId, + subTaskId + }); + } +} diff --git a/src/modules/tenancy/application/commands/onboard-tenant/onboard-tenant.command.ts b/src/modules/tenancy/application/commands/onboard-tenant/onboard-tenant.command.ts new file mode 100644 index 0000000..b086d6d --- /dev/null +++ b/src/modules/tenancy/application/commands/onboard-tenant/onboard-tenant.command.ts @@ -0,0 +1,5 @@ +export interface OnboardTenantCommand { + name: string; + slug: string; + companyName?: string; +} diff --git a/src/modules/tenancy/application/commands/onboard-tenant/onboard-tenant.dto.ts b/src/modules/tenancy/application/commands/onboard-tenant/onboard-tenant.dto.ts new file mode 100644 index 0000000..5951e01 --- /dev/null +++ b/src/modules/tenancy/application/commands/onboard-tenant/onboard-tenant.dto.ts @@ -0,0 +1,9 @@ +import { TenantResponseDto } from "../../mappers/tenant-response.mapper"; + +export class OnboardTenantRequestDto { + name!: string; + slug!: string; + companyName?: string; +} + +export { TenantResponseDto }; diff --git a/src/modules/tenancy/application/commands/onboard-tenant/onboard-tenant.handler.ts b/src/modules/tenancy/application/commands/onboard-tenant/onboard-tenant.handler.ts new file mode 100644 index 0000000..51f253d --- /dev/null +++ b/src/modules/tenancy/application/commands/onboard-tenant/onboard-tenant.handler.ts @@ -0,0 +1,54 @@ +import { UseCase } from "../../../../../shared/application/common/use-case"; +import { ClockPort } from "../../../../../shared/application/ports/clock.port"; +import { IdGeneratorPort } from "../../../../../shared/application/ports/id-generator.port"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { TenantAggregate } from "../../../domain/aggregates/tenant.aggregate"; +import { TenantRepository } from "../../../domain/repositories/tenant.repository"; +import { TenantProvisionerPort } from "../../ports/tenant-provisioner.port"; +import { + TenantResponseDto, + toTenantResponse +} from "../../mappers/tenant-response.mapper"; +import { OnboardTenantCommand } from "./onboard-tenant.command"; +import { InitializeSubscriptionHandler } from "../../../../billing/application/commands/initialize-subscription/initialize-subscription.handler"; + +export class OnboardTenantHandler + implements UseCase +{ + constructor( + private readonly tenantRepository: TenantRepository, + private readonly tenantProvisioner: TenantProvisionerPort, + private readonly initializeSubscription: InitializeSubscriptionHandler, + private readonly clock: ClockPort, + private readonly idGenerator: IdGeneratorPort + ) {} + + public async execute( + command: OnboardTenantCommand + ): Promise { + const existingTenant = await this.tenantRepository.findBySlug(command.slug); + + if (existingTenant) { + throw new ApplicationError( + "A tenant with the same slug already exists", + 409, + "TENANT_SLUG_CONFLICT" + ); + } + + const tenant = TenantAggregate.onboard({ + id: this.idGenerator.generate(), + companyId: this.idGenerator.generate(), + name: command.name, + slug: command.slug, + companyName: command.companyName, + now: this.clock.now() + }); + + await this.tenantRepository.save(tenant); + await this.tenantProvisioner.provisionTenant(tenant.id); + await this.initializeSubscription.execute({ tenantId: tenant.id }); + + return toTenantResponse(tenant); + } +} diff --git a/src/modules/tenancy/application/mappers/tenant-response.mapper.ts b/src/modules/tenancy/application/mappers/tenant-response.mapper.ts new file mode 100644 index 0000000..1cdbb21 --- /dev/null +++ b/src/modules/tenancy/application/mappers/tenant-response.mapper.ts @@ -0,0 +1,38 @@ +import { + TenantAggregate, + TenantStatus +} from "../../domain/aggregates/tenant.aggregate"; + +export class CompanyResponseDto { + id!: string; + name!: string; + slug!: string; +} + +export class TenantResponseDto { + id!: string; + name!: string; + slug!: string; + status!: TenantStatus; + company!: CompanyResponseDto; + createdAt!: string; + updatedAt!: string; +} + +export function toTenantResponse( + aggregate: TenantAggregate +): TenantResponseDto { + return { + id: aggregate.id, + name: aggregate.name, + slug: aggregate.slug, + status: aggregate.status, + company: { + id: aggregate.company.id, + name: aggregate.company.name, + slug: aggregate.company.slug + }, + createdAt: aggregate.createdAt.toISOString(), + updatedAt: aggregate.updatedAt.toISOString() + }; +} diff --git a/src/modules/tenancy/application/ports/tenant-provisioner.port.ts b/src/modules/tenancy/application/ports/tenant-provisioner.port.ts new file mode 100644 index 0000000..77a31b9 --- /dev/null +++ b/src/modules/tenancy/application/ports/tenant-provisioner.port.ts @@ -0,0 +1,3 @@ +export interface TenantProvisionerPort { + provisionTenant(tenantId: string): Promise; +} diff --git a/src/modules/tenancy/application/queries/get-tenant-by-id/get-tenant-by-id.handler.ts b/src/modules/tenancy/application/queries/get-tenant-by-id/get-tenant-by-id.handler.ts new file mode 100644 index 0000000..6198568 --- /dev/null +++ b/src/modules/tenancy/application/queries/get-tenant-by-id/get-tenant-by-id.handler.ts @@ -0,0 +1,26 @@ +import { UseCase } from "../../../../../shared/application/common/use-case"; +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { TenantRepository } from "../../../domain/repositories/tenant.repository"; +import { + TenantResponseDto, + toTenantResponse +} from "../../mappers/tenant-response.mapper"; +import { GetTenantByIdQuery } from "./get-tenant-by-id.query"; + +export class GetTenantByIdHandler + implements UseCase +{ + constructor(private readonly tenantRepository: TenantRepository) {} + + public async execute( + query: GetTenantByIdQuery + ): Promise { + const tenant = await this.tenantRepository.findById(query.tenantId); + + if (!tenant) { + throw new ApplicationError("Tenant was not found", 404, "TENANT_NOT_FOUND"); + } + + return toTenantResponse(tenant); + } +} diff --git a/src/modules/tenancy/application/queries/get-tenant-by-id/get-tenant-by-id.query.ts b/src/modules/tenancy/application/queries/get-tenant-by-id/get-tenant-by-id.query.ts new file mode 100644 index 0000000..11870f1 --- /dev/null +++ b/src/modules/tenancy/application/queries/get-tenant-by-id/get-tenant-by-id.query.ts @@ -0,0 +1,3 @@ +export interface GetTenantByIdQuery { + tenantId: string; +} diff --git a/src/modules/tenancy/application/queries/resolve-tenant/resolve-tenant.handler.ts b/src/modules/tenancy/application/queries/resolve-tenant/resolve-tenant.handler.ts new file mode 100644 index 0000000..82ec0fa --- /dev/null +++ b/src/modules/tenancy/application/queries/resolve-tenant/resolve-tenant.handler.ts @@ -0,0 +1,62 @@ +import { ApplicationError } from "../../../../../shared/domain/errors/application-error"; +import { TenantContext } from "../../../../../shared/domain/types/tenant-context"; +import { TenantRepository } from "../../../domain/repositories/tenant.repository"; +import { ResolveTenantQuery } from "./resolve-tenant.query"; + +export class ResolveTenantHandler { + constructor(private readonly tenantRepository: TenantRepository) {} + + public async execute(query: ResolveTenantQuery): Promise { + const tenantById = query.tenantId + ? await this.tenantRepository.findById(query.tenantId) + : null; + const tenantBySlug = query.tenantSlug + ? await this.tenantRepository.findBySlug(query.tenantSlug) + : null; + + if (query.tenantId && !tenantById) { + throw new ApplicationError("Tenant not found", 404, "TENANT_NOT_FOUND"); + } + + if (query.tenantSlug && !tenantBySlug) { + throw new ApplicationError("Tenant not found", 404, "TENANT_NOT_FOUND"); + } + + if (!tenantById && !tenantBySlug) { + return null; + } + + if (tenantById && tenantBySlug && tenantById.id !== tenantBySlug.id) { + throw new ApplicationError( + "Tenant context is inconsistent", + 403, + "TENANT_CONTEXT_MISMATCH" + ); + } + + const tenant = tenantById ?? tenantBySlug; + + if (!tenant) { + return null; + } + + if (tenant.status !== "active") { + throw new ApplicationError( + "Tenant is inactive", + 403, + "TENANT_INACTIVE" + ); + } + + return { + tenantId: tenant.id, + tenantSlug: tenant.slug, + companyId: tenant.company.id, + companyName: tenant.company.name, + schemaName: + query.mode === "schema-per-tenant" ? tenant.slug : undefined, + mode: query.mode, + isResolved: true + }; + } +} diff --git a/src/modules/tenancy/application/queries/resolve-tenant/resolve-tenant.query.ts b/src/modules/tenancy/application/queries/resolve-tenant/resolve-tenant.query.ts new file mode 100644 index 0000000..2c38147 --- /dev/null +++ b/src/modules/tenancy/application/queries/resolve-tenant/resolve-tenant.query.ts @@ -0,0 +1,7 @@ +import { TenantContext } from "../../../../../shared/domain/types/tenant-context"; + +export interface ResolveTenantQuery { + tenantId?: string; + tenantSlug?: string; + mode: TenantContext["mode"]; +} diff --git a/src/modules/tenancy/domain/aggregates/tenant.aggregate.ts b/src/modules/tenancy/domain/aggregates/tenant.aggregate.ts new file mode 100644 index 0000000..c4e8cc5 --- /dev/null +++ b/src/modules/tenancy/domain/aggregates/tenant.aggregate.ts @@ -0,0 +1,129 @@ +import { AggregateRoot } from "../../../../shared/domain/base/aggregate-root"; +import { DomainError } from "../../../../shared/domain/errors/domain-error"; +import { CompanyEntity, CompanyPrimitives } from "../entities/company.entity"; +import { TenantOnboardedEvent } from "../events/tenant-onboarded.event"; +import { TenantId } from "../value-objects/tenant-id.vo"; + +export type TenantStatus = "active" | "suspended"; + +export interface TenantPrimitives { + id: string; + name: string; + slug: string; + status: TenantStatus; + company: CompanyPrimitives; + createdAt: Date; + updatedAt: Date; +} + +interface OnboardTenantProps { + id: string; + name: string; + slug: string; + companyName?: string; + companyId: string; + now: Date; +} + +export class TenantAggregate extends AggregateRoot { + private readonly companyState: CompanyEntity; + + private constructor(private readonly state: TenantPrimitives) { + super(state.id); + this.companyState = new CompanyEntity(state.company); + } + + public static onboard(props: OnboardTenantProps): TenantAggregate { + const normalizedName = props.name.trim(); + const normalizedSlug = props.slug.trim().toLowerCase(); + const normalizedCompanyName = (props.companyName ?? props.name).trim(); + + if (!normalizedName) { + throw new DomainError("Tenant name is required"); + } + + if (!normalizedCompanyName) { + throw new DomainError("Company name is required"); + } + + if (!/^[a-z0-9-]{3,50}$/.test(normalizedSlug)) { + throw new DomainError( + "Tenant slug must be 3-50 characters and contain only lowercase letters, numbers, or dashes" + ); + } + + const aggregate = new TenantAggregate({ + id: TenantId.create(props.id).value, + name: normalizedName, + slug: normalizedSlug, + status: "active", + company: { + id: props.companyId, + tenantId: TenantId.create(props.id).value, + name: normalizedCompanyName, + slug: normalizedSlug, + createdAt: props.now, + updatedAt: props.now + }, + createdAt: props.now, + updatedAt: props.now + }); + + aggregate.addDomainEvent( + new TenantOnboardedEvent(aggregate.id, aggregate.slug) + ); + + return aggregate; + } + + public static rehydrate(primitives: TenantPrimitives): TenantAggregate { + return new TenantAggregate({ + ...primitives, + id: TenantId.create(primitives.id).value, + company: { + ...primitives.company, + tenantId: TenantId.create(primitives.company.tenantId).value + } + }); + } + + public suspend(now: Date): void { + if (this.state.status === "suspended") { + throw new DomainError("Tenant is already suspended"); + } + + this.state.status = "suspended"; + this.state.updatedAt = now; + } + + public get name(): string { + return this.state.name; + } + + public get slug(): string { + return this.state.slug; + } + + public get status(): TenantStatus { + return this.state.status; + } + + public get company(): CompanyEntity { + return this.companyState; + } + + public get createdAt(): Date { + return this.state.createdAt; + } + + public get updatedAt(): Date { + return this.state.updatedAt; + } + + public toPrimitives(): TenantPrimitives { + return { + ...this.state, + company: this.companyState.toPrimitives() + }; + } +} diff --git a/src/modules/tenancy/domain/entities/company.entity.ts b/src/modules/tenancy/domain/entities/company.entity.ts new file mode 100644 index 0000000..5cbcc72 --- /dev/null +++ b/src/modules/tenancy/domain/entities/company.entity.ts @@ -0,0 +1,40 @@ +import { Entity } from "../../../../shared/domain/base/entity"; + +export interface CompanyPrimitives { + id: string; + tenantId: string; + name: string; + slug: string; + createdAt: Date; + updatedAt: Date; +} + +export class CompanyEntity extends Entity { + constructor(private readonly state: CompanyPrimitives) { + super(state.id); + } + + public get tenantId(): string { + return this.state.tenantId; + } + + public get name(): string { + return this.state.name; + } + + public get slug(): string { + return this.state.slug; + } + + public get createdAt(): Date { + return this.state.createdAt; + } + + public get updatedAt(): Date { + return this.state.updatedAt; + } + + public toPrimitives(): CompanyPrimitives { + return { ...this.state }; + } +} diff --git a/src/modules/tenancy/domain/events/tenant-onboarded.event.ts b/src/modules/tenancy/domain/events/tenant-onboarded.event.ts new file mode 100644 index 0000000..a21b56b --- /dev/null +++ b/src/modules/tenancy/domain/events/tenant-onboarded.event.ts @@ -0,0 +1,13 @@ +import { DomainEvent } from "../../../../shared/domain/base/domain-event"; + +export class TenantOnboardedEvent implements DomainEvent { + public readonly name = "tenant.onboarded"; + public readonly occurredOn: Date; + + constructor( + public readonly tenantId: string, + public readonly tenantSlug: string + ) { + this.occurredOn = new Date(); + } +} diff --git a/src/modules/tenancy/domain/repositories/tenant.repository.ts b/src/modules/tenancy/domain/repositories/tenant.repository.ts new file mode 100644 index 0000000..3421e13 --- /dev/null +++ b/src/modules/tenancy/domain/repositories/tenant.repository.ts @@ -0,0 +1,7 @@ +import { Repository } from "../../../../shared/domain/base/repository"; +import { TenantAggregate } from "../aggregates/tenant.aggregate"; + +export interface TenantRepository + extends Repository { + findBySlug(slug: string): Promise; +} diff --git a/src/modules/tenancy/domain/value-objects/tenant-id.vo.ts b/src/modules/tenancy/domain/value-objects/tenant-id.vo.ts new file mode 100644 index 0000000..090ebea --- /dev/null +++ b/src/modules/tenancy/domain/value-objects/tenant-id.vo.ts @@ -0,0 +1,26 @@ +import { ValueObject } from "../../../../shared/domain/base/value-object"; +import { DomainError } from "../../../../shared/domain/errors/domain-error"; + +interface TenantIdProps { + value: string; +} + +export class TenantId extends ValueObject { + private constructor(props: TenantIdProps) { + super(props); + } + + public static create(value: string): TenantId { + const normalized = value.trim(); + + if (!normalized) { + throw new DomainError("Tenant id cannot be empty"); + } + + return new TenantId({ value: normalized }); + } + + public get value(): string { + return this.props.value; + } +} diff --git a/src/modules/tenancy/infrastructure/persistence/typeorm/entities/company.orm-entity.ts b/src/modules/tenancy/infrastructure/persistence/typeorm/entities/company.orm-entity.ts new file mode 100644 index 0000000..9291db8 --- /dev/null +++ b/src/modules/tenancy/infrastructure/persistence/typeorm/entities/company.orm-entity.ts @@ -0,0 +1,32 @@ +import { Column, Entity, Index, JoinColumn, OneToOne, PrimaryColumn } from "typeorm"; + +import { TenantOrmEntity } from "./tenant.orm-entity"; + +@Entity({ name: "companies" }) +@Index(["tenantId"], { unique: true }) +@Index(["slug"], { unique: true }) +export class CompanyOrmEntity { + @PrimaryColumn("uuid") + id!: string; + + @Column({ type: "uuid", name: "tenant_id" }) + tenantId!: string; + + @Column({ type: "varchar", length: 120 }) + name!: string; + + @Column({ type: "varchar", length: 64 }) + slug!: string; + + @Column({ type: "timestamptz", name: "created_at" }) + createdAt!: Date; + + @Column({ type: "timestamptz", name: "updated_at" }) + updatedAt!: Date; + + @OneToOne(() => TenantOrmEntity, (tenant) => tenant.company, { + onDelete: "CASCADE" + }) + @JoinColumn({ name: "tenant_id" }) + tenant!: TenantOrmEntity; +} diff --git a/src/modules/tenancy/infrastructure/persistence/typeorm/entities/tenant.orm-entity.ts b/src/modules/tenancy/infrastructure/persistence/typeorm/entities/tenant.orm-entity.ts new file mode 100644 index 0000000..9de0250 --- /dev/null +++ b/src/modules/tenancy/infrastructure/persistence/typeorm/entities/tenant.orm-entity.ts @@ -0,0 +1,30 @@ +import { Column, Entity, OneToOne, PrimaryColumn } from "typeorm"; + +import { CompanyOrmEntity } from "./company.orm-entity"; + +@Entity({ name: "tenants" }) +export class TenantOrmEntity { + @PrimaryColumn("uuid") + id!: string; + + @Column({ type: "varchar", length: 120 }) + name!: string; + + @Column({ type: "varchar", length: 64, unique: true }) + slug!: string; + + @Column({ type: "varchar", length: 20 }) + status!: string; + + @Column({ type: "timestamptz", name: "created_at" }) + createdAt!: Date; + + @Column({ type: "timestamptz", name: "updated_at" }) + updatedAt!: Date; + + @OneToOne(() => CompanyOrmEntity, (company) => company.tenant, { + cascade: true, + eager: true + }) + company!: CompanyOrmEntity; +} diff --git a/src/modules/tenancy/infrastructure/persistence/typeorm/mappers/tenant-persistence.mapper.ts b/src/modules/tenancy/infrastructure/persistence/typeorm/mappers/tenant-persistence.mapper.ts new file mode 100644 index 0000000..373b77c --- /dev/null +++ b/src/modules/tenancy/infrastructure/persistence/typeorm/mappers/tenant-persistence.mapper.ts @@ -0,0 +1,49 @@ +import { TenantAggregate } from "../../../../domain/aggregates/tenant.aggregate"; +import { CompanyOrmEntity } from "../entities/company.orm-entity"; +import { TenantOrmEntity } from "../entities/tenant.orm-entity"; + +export function toTenantDomain(entity: TenantOrmEntity): TenantAggregate { + return TenantAggregate.rehydrate({ + id: entity.id, + name: entity.name, + slug: entity.slug, + status: entity.status === "suspended" ? "suspended" : "active", + company: { + id: entity.company.id, + tenantId: entity.company.tenantId, + name: entity.company.name, + slug: entity.company.slug, + createdAt: entity.company.createdAt, + updatedAt: entity.company.updatedAt + }, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt + }); +} + +export function toTenantPersistence( + aggregate: TenantAggregate +): TenantOrmEntity { + const primitives = aggregate.toPrimitives(); + const entity = new TenantOrmEntity(); + const company = new CompanyOrmEntity(); + + entity.id = primitives.id; + entity.name = primitives.name; + entity.slug = primitives.slug; + entity.status = primitives.status; + entity.createdAt = primitives.createdAt; + entity.updatedAt = primitives.updatedAt; + + company.id = primitives.company.id; + company.tenantId = primitives.company.tenantId; + company.name = primitives.company.name; + company.slug = primitives.company.slug; + company.createdAt = primitives.company.createdAt; + company.updatedAt = primitives.company.updatedAt; + company.tenant = entity; + + entity.company = company; + + return entity; +} diff --git a/src/modules/tenancy/infrastructure/persistence/typeorm/repositories/typeorm-tenant.repository.ts b/src/modules/tenancy/infrastructure/persistence/typeorm/repositories/typeorm-tenant.repository.ts new file mode 100644 index 0000000..0a2356c --- /dev/null +++ b/src/modules/tenancy/infrastructure/persistence/typeorm/repositories/typeorm-tenant.repository.ts @@ -0,0 +1,50 @@ +import { Repository as TypeOrmRepository } from "typeorm"; + +import { TenantDataSourceResolver } from "../../../../../../shared/infrastructure/persistence/typeorm/tenant-data-source.resolver"; +import { TenantAggregate } from "../../../../domain/aggregates/tenant.aggregate"; +import { TenantRepository } from "../../../../domain/repositories/tenant.repository"; +import { TenantOrmEntity } from "../entities/tenant.orm-entity"; +import { + toTenantDomain, + toTenantPersistence +} from "../mappers/tenant-persistence.mapper"; + +export class TypeOrmTenantRepository implements TenantRepository { + constructor( + private readonly tenantDataSourceResolver: TenantDataSourceResolver + ) {} + + public async findById(id: string): Promise { + const repository = await this.getRepository(); + const entity = await repository.findOne({ + where: { id }, + relations: { + company: true + } + }); + + return entity ? toTenantDomain(entity) : null; + } + + public async findBySlug(slug: string): Promise { + const repository = await this.getRepository(); + const entity = await repository.findOne({ + where: { slug: slug.toLowerCase() }, + relations: { + company: true + } + }); + + return entity ? toTenantDomain(entity) : null; + } + + public async save(aggregate: TenantAggregate): Promise { + const repository = await this.getRepository(); + await repository.save(toTenantPersistence(aggregate)); + } + + private async getRepository(): Promise> { + const dataSource = await this.tenantDataSourceResolver.getPlatformDataSource(); + return dataSource.getRepository(TenantOrmEntity); + } +} diff --git a/src/modules/tenancy/interfaces/http/controllers/tenants.controller.ts b/src/modules/tenancy/interfaces/http/controllers/tenants.controller.ts new file mode 100644 index 0000000..32f91c8 --- /dev/null +++ b/src/modules/tenancy/interfaces/http/controllers/tenants.controller.ts @@ -0,0 +1,45 @@ +import { + Body, + Controller, + Get, + Path, + Post, + Response, + Route, + SuccessResponse, + Tags +} from "tsoa"; + +import { OnboardTenantRequestDto } from "../../../application/commands/onboard-tenant/onboard-tenant.dto"; +import { OnboardTenantHandler } from "../../../application/commands/onboard-tenant/onboard-tenant.handler"; +import { GetTenantByIdHandler } from "../../../application/queries/get-tenant-by-id/get-tenant-by-id.handler"; +import { TenantResponseDto } from "../../../application/mappers/tenant-response.mapper"; + +@Route("v1/tenants") +@Tags("Tenants") +export class TenantsController extends Controller { + constructor( + private readonly onboardTenantHandler: OnboardTenantHandler, + private readonly getTenantByIdHandler: GetTenantByIdHandler + ) { + super(); + } + + @Post() + @SuccessResponse("201", "Created") + @Response("409", "Tenant slug already exists") + public async onboardTenant( + @Body() requestBody: OnboardTenantRequestDto + ): Promise { + this.setStatus(201); + return this.onboardTenantHandler.execute(requestBody); + } + + @Get("{tenantId}") + @Response("404", "Tenant not found") + public async getTenantById( + @Path() tenantId: string + ): Promise { + return this.getTenantByIdHandler.execute({ tenantId }); + } +} diff --git a/src/shared/application/common/use-case.ts b/src/shared/application/common/use-case.ts new file mode 100644 index 0000000..6e99c93 --- /dev/null +++ b/src/shared/application/common/use-case.ts @@ -0,0 +1,3 @@ +export interface UseCase { + execute(request: TRequest): Promise; +} diff --git a/src/shared/application/ports/clock.port.ts b/src/shared/application/ports/clock.port.ts new file mode 100644 index 0000000..362caf7 --- /dev/null +++ b/src/shared/application/ports/clock.port.ts @@ -0,0 +1,3 @@ +export interface ClockPort { + now(): Date; +} diff --git a/src/shared/application/ports/id-generator.port.ts b/src/shared/application/ports/id-generator.port.ts new file mode 100644 index 0000000..ac448ef --- /dev/null +++ b/src/shared/application/ports/id-generator.port.ts @@ -0,0 +1,3 @@ +export interface IdGeneratorPort { + generate(): string; +} diff --git a/src/shared/application/ports/password-hasher.port.ts b/src/shared/application/ports/password-hasher.port.ts new file mode 100644 index 0000000..fad7e52 --- /dev/null +++ b/src/shared/application/ports/password-hasher.port.ts @@ -0,0 +1,4 @@ +export interface PasswordHasherPort { + hash(plainText: string): Promise; + compare(plainText: string, hash: string): Promise; +} diff --git a/src/shared/domain/base/aggregate-root.ts b/src/shared/domain/base/aggregate-root.ts new file mode 100644 index 0000000..1cafee7 --- /dev/null +++ b/src/shared/domain/base/aggregate-root.ts @@ -0,0 +1,17 @@ +import { Entity } from "./entity"; +import { DomainEvent } from "./domain-event"; + +export abstract class AggregateRoot extends Entity { + private readonly domainEvents: DomainEvent[] = []; + + protected addDomainEvent(event: DomainEvent): void { + this.domainEvents.push(event); + } + + public pullDomainEvents(): DomainEvent[] { + const events = [...this.domainEvents]; + this.domainEvents.length = 0; + + return events; + } +} diff --git a/src/shared/domain/base/domain-event.ts b/src/shared/domain/base/domain-event.ts new file mode 100644 index 0000000..802cab3 --- /dev/null +++ b/src/shared/domain/base/domain-event.ts @@ -0,0 +1,4 @@ +export interface DomainEvent { + readonly name: string; + readonly occurredOn: Date; +} diff --git a/src/shared/domain/base/entity.ts b/src/shared/domain/base/entity.ts new file mode 100644 index 0000000..61df652 --- /dev/null +++ b/src/shared/domain/base/entity.ts @@ -0,0 +1,3 @@ +export abstract class Entity { + protected constructor(public readonly id: TId) {} +} diff --git a/src/shared/domain/base/repository.ts b/src/shared/domain/base/repository.ts new file mode 100644 index 0000000..a05b2e1 --- /dev/null +++ b/src/shared/domain/base/repository.ts @@ -0,0 +1,4 @@ +export interface Repository { + findById(id: TId): Promise; + save(aggregate: TAggregate): Promise; +} diff --git a/src/shared/domain/base/value-object.ts b/src/shared/domain/base/value-object.ts new file mode 100644 index 0000000..14960f3 --- /dev/null +++ b/src/shared/domain/base/value-object.ts @@ -0,0 +1,11 @@ +export abstract class ValueObject { + protected constructor(protected readonly props: TProps) {} + + public equals(other?: ValueObject): boolean { + if (!other) { + return false; + } + + return JSON.stringify(this.props) === JSON.stringify(other.props); + } +} diff --git a/src/shared/domain/errors/application-error.ts b/src/shared/domain/errors/application-error.ts new file mode 100644 index 0000000..2005704 --- /dev/null +++ b/src/shared/domain/errors/application-error.ts @@ -0,0 +1,10 @@ +export class ApplicationError extends Error { + constructor( + message: string, + public readonly statusCode = 400, + public readonly code = "APPLICATION_ERROR" + ) { + super(message); + this.name = "ApplicationError"; + } +} diff --git a/src/shared/domain/errors/domain-error.ts b/src/shared/domain/errors/domain-error.ts new file mode 100644 index 0000000..9b90a61 --- /dev/null +++ b/src/shared/domain/errors/domain-error.ts @@ -0,0 +1,9 @@ +export class DomainError extends Error { + constructor( + message: string, + public readonly code = "DOMAIN_ERROR" + ) { + super(message); + this.name = "DomainError"; + } +} diff --git a/src/shared/domain/types/request-context.ts b/src/shared/domain/types/request-context.ts new file mode 100644 index 0000000..0ebed27 --- /dev/null +++ b/src/shared/domain/types/request-context.ts @@ -0,0 +1,15 @@ +import { TenantContext } from "./tenant-context"; + +export interface AuthPrincipal { + subject: string; + email: string; + roles: string[]; + permissions: string[]; + tenantId?: string; +} + +export interface RequestContext { + requestId: string; + tenant?: TenantContext; + principal?: AuthPrincipal; +} diff --git a/src/shared/domain/types/tenant-context.ts b/src/shared/domain/types/tenant-context.ts new file mode 100644 index 0000000..f2585a8 --- /dev/null +++ b/src/shared/domain/types/tenant-context.ts @@ -0,0 +1,14 @@ +export type TenancyMode = + | "shared-schema" + | "schema-per-tenant" + | "database-per-tenant"; + +export interface TenantContext { + tenantId?: string; + tenantSlug?: string; + companyId?: string; + companyName?: string; + schemaName?: string; + mode: TenancyMode; + isResolved?: boolean; +} diff --git a/src/shared/infrastructure/config/app-config.ts b/src/shared/infrastructure/config/app-config.ts new file mode 100644 index 0000000..ea82304 --- /dev/null +++ b/src/shared/infrastructure/config/app-config.ts @@ -0,0 +1,49 @@ +import { TenancyMode } from "../../domain/types/tenant-context"; + +export interface AppConfig { + environment: string; + application: { + name: string; + }; + http: { + port: number; + apiBasePath: string; + swaggerPath: string; + }; + security: { + jwtSecret: string; + jwtExpiresIn: string; + jwtIssuer: string; + jwtAudience: string; + bcryptRounds: number; + }; + database: { + host: string; + port: number; + name: string; + username: string; + password: string; + schema: string; + ssl: boolean; + synchronize: boolean; + logging: boolean; + initializeOnBootstrap: boolean; + }; + tenancy: { + mode: TenancyMode; + }; + storage: { + provider: "local" | "s3"; + local: { + basePath: string; + }; + s3: { + region: string; + bucket: string; + endpoint?: string; + accessKeyId?: string; + secretAccessKey?: string; + forcePathStyle?: boolean; + }; + }; +} diff --git a/src/shared/infrastructure/config/env.ts b/src/shared/infrastructure/config/env.ts new file mode 100644 index 0000000..9b43192 --- /dev/null +++ b/src/shared/infrastructure/config/env.ts @@ -0,0 +1,91 @@ +import dotenv from "dotenv"; + +import { AppConfig } from "./app-config"; + +dotenv.config(); + +function readString(name: string, fallback?: string): string { + const value = process.env[name] ?? fallback; + + if (!value) { + throw new Error(`Missing required environment variable: ${name}`); + } + + return value; +} + +function readNumber(name: string, fallback: number): number { + const value = process.env[name]; + + if (!value) { + return fallback; + } + + const parsed = Number(value); + + if (Number.isNaN(parsed)) { + throw new Error(`Environment variable ${name} must be a number`); + } + + return parsed; +} + +function readBoolean(name: string, fallback: boolean): boolean { + const value = process.env[name]; + + if (!value) { + return fallback; + } + + return value.toLowerCase() === "true"; +} + +export function loadAppConfig(): AppConfig { + return { + environment: readString("NODE_ENV", "development"), + application: { + name: readString("APP_NAME", "gem-track-backend") + }, + http: { + port: readNumber("PORT", 3000), + apiBasePath: readString("API_BASE_PATH", "/api"), + swaggerPath: readString("SWAGGER_PATH", "/docs") + }, + security: { + jwtSecret: readString("JWT_SECRET", "change-me"), + jwtExpiresIn: readString("JWT_EXPIRES_IN", "1h"), + jwtIssuer: readString("JWT_ISSUER", "gem-track-backend"), + jwtAudience: readString("JWT_AUDIENCE", "gem-track-api"), + bcryptRounds: readNumber("BCRYPT_ROUNDS", 12) + }, + database: { + host: readString("DB_HOST", "localhost"), + port: readNumber("DB_PORT", 5432), + name: readString("DB_NAME", "gem_track"), + username: readString("DB_USER", "postgres"), + password: readString("DB_PASSWORD", "postgres"), + schema: readString("DB_SCHEMA", "public"), + ssl: readBoolean("DB_SSL", false), + synchronize: readBoolean("TYPEORM_SYNCHRONIZE", false), + logging: readBoolean("TYPEORM_LOGGING", false), + initializeOnBootstrap: readBoolean("DB_INITIALIZE_ON_BOOTSTRAP", false) + }, + tenancy: { + mode: readString("TENANCY_MODE", "shared-schema") as AppConfig["tenancy"]["mode"] + }, + storage: { + provider: (process.env.STORAGE_PROVIDER ?? "local") as "local" | "s3", + local: { + basePath: readString("STORAGE_LOCAL_BASE_PATH", "uploads") + }, + s3: { + region: readString("STORAGE_S3_REGION", "us-east-1"), + bucket: readString("STORAGE_S3_BUCKET", "gem-track-docs"), + endpoint: process.env.STORAGE_S3_ENDPOINT, + accessKeyId: process.env.STORAGE_S3_ACCESS_KEY_ID, + secretAccessKey: process.env.STORAGE_S3_SECRET_ACCESS_KEY, + forcePathStyle: readBoolean("STORAGE_S3_FORCE_PATH_STYLE", false) + } + } + }; +} diff --git a/src/shared/infrastructure/logging/logger.ts b/src/shared/infrastructure/logging/logger.ts new file mode 100644 index 0000000..ac6e399 --- /dev/null +++ b/src/shared/infrastructure/logging/logger.ts @@ -0,0 +1,31 @@ +export class Logger { + constructor(private readonly serviceName: string) {} + + public info(message: string, context?: Record): void { + this.write("INFO", message, context); + } + + public warn(message: string, context?: Record): void { + this.write("WARN", message, context); + } + + public error(message: string, context?: Record): void { + this.write("ERROR", message, context); + } + + private write( + level: "INFO" | "WARN" | "ERROR", + message: string, + context?: Record + ): void { + const payload = { + timestamp: new Date().toISOString(), + level, + service: this.serviceName, + message, + context + }; + + console.log(JSON.stringify(payload)); + } +} diff --git a/src/shared/infrastructure/persistence/typeorm/data-source.factory.ts b/src/shared/infrastructure/persistence/typeorm/data-source.factory.ts new file mode 100644 index 0000000..5b465e4 --- /dev/null +++ b/src/shared/infrastructure/persistence/typeorm/data-source.factory.ts @@ -0,0 +1,147 @@ +import { PermissionOrmEntity } from "../../../../modules/identity-access/infrastructure/persistence/typeorm/entities/permission.orm-entity"; +import { RoleOrmEntity } from "../../../../modules/identity-access/infrastructure/persistence/typeorm/entities/role.orm-entity"; +import { RolePermissionOrmEntity } from "../../../../modules/identity-access/infrastructure/persistence/typeorm/entities/role-permission.orm-entity"; +import { UserOrmEntity } from "../../../../modules/identity-access/infrastructure/persistence/typeorm/entities/user.orm-entity"; +import { UserRoleOrmEntity } from "../../../../modules/identity-access/infrastructure/persistence/typeorm/entities/user-role.orm-entity"; +import { SubTaskOrmEntity } from "../../../../modules/task-management/infrastructure/persistence/typeorm/entities/subtask.orm-entity"; +import { TaskOrmEntity } from "../../../../modules/task-management/infrastructure/persistence/typeorm/entities/task.orm-entity"; +import { CompanyOrmEntity } from "../../../../modules/tenancy/infrastructure/persistence/typeorm/entities/company.orm-entity"; +import { TenantOrmEntity } from "../../../../modules/tenancy/infrastructure/persistence/typeorm/entities/tenant.orm-entity"; +import { BillingRecordOrmEntity } from "../../../../modules/billing/infrastructure/persistence/typeorm/entities/billing-record.orm-entity"; +import { SubscriptionPlanOrmEntity } from "../../../../modules/billing/infrastructure/persistence/typeorm/entities/subscription-plan.orm-entity"; +import { TenantSubscriptionOrmEntity } from "../../../../modules/billing/infrastructure/persistence/typeorm/entities/tenant-subscription.orm-entity"; +import { DocumentOrmEntity } from "../../../../modules/document-management/infrastructure/persistence/typeorm/entities/document.orm-entity"; +import { DocumentLinkOrmEntity } from "../../../../modules/document-management/infrastructure/persistence/typeorm/entities/document-link.orm-entity"; +import { TenantContext } from "../../../domain/types/tenant-context"; +import { AppConfig } from "../../config/app-config"; +import { Logger } from "../../logging/logger"; +import { DataSource } from "typeorm"; +import { PostgresConnectionOptions } from "typeorm/driver/postgres/PostgresConnectionOptions"; + +export class TypeOrmDataSourceFactory { + private platformDataSource?: Promise; + private readonly tenantDataSources = new Map>(); + + constructor( + private readonly config: AppConfig, + private readonly logger: Logger + ) {} + + public async getPlatformDataSource(): Promise { + if (!this.platformDataSource) { + this.platformDataSource = this.initializeDataSource( + this.buildBaseOptions() + ); + } + + return this.platformDataSource; + } + + public async getTenantDataSource( + tenantContext: TenantContext + ): Promise { + if (tenantContext.mode === "shared-schema") { + return this.getPlatformDataSource(); + } + + const cacheKey = tenantContext.schemaName ?? tenantContext.tenantId; + + if (!cacheKey) { + throw new Error( + "A tenant-specific data source requires a schemaName or tenantId" + ); + } + + if (!this.tenantDataSources.has(cacheKey)) { + this.tenantDataSources.set( + cacheKey, + this.initializeDataSource( + this.buildBaseOptions({ + schema: + tenantContext.mode === "schema-per-tenant" + ? tenantContext.schemaName ?? tenantContext.tenantId + : this.config.database.schema, + database: + tenantContext.mode === "database-per-tenant" + ? tenantContext.tenantId ?? this.config.database.name + : this.config.database.name + }) + ) + ); + } + + return this.tenantDataSources.get(cacheKey) as Promise; + } + + public async destroy(): Promise { + const dataSources = [ + this.platformDataSource, + ...this.tenantDataSources.values() + ].filter( + (dataSource): dataSource is Promise => dataSource !== undefined + ); + + await Promise.all( + dataSources.map(async (dataSourcePromise) => { + const dataSource = await dataSourcePromise; + + if (dataSource.isInitialized) { + await dataSource.destroy(); + } + }) + ); + } + + private buildBaseOptions( + overrides?: Partial + ): PostgresConnectionOptions { + return { + type: "postgres", + host: this.config.database.host, + port: this.config.database.port, + database: this.config.database.name, + username: this.config.database.username, + password: this.config.database.password, + schema: this.config.database.schema, + ssl: this.config.database.ssl, + synchronize: this.config.database.synchronize, + logging: this.config.database.logging, + invalidWhereValuesBehavior: { + null: "throw", + undefined: "throw" + }, + entities: [ + TenantOrmEntity, + CompanyOrmEntity, + UserOrmEntity, + RoleOrmEntity, + PermissionOrmEntity, + UserRoleOrmEntity, + RolePermissionOrmEntity, + TaskOrmEntity, + SubTaskOrmEntity, + BillingRecordOrmEntity, + SubscriptionPlanOrmEntity, + TenantSubscriptionOrmEntity, + DocumentOrmEntity, + DocumentLinkOrmEntity + ], + ...overrides + }; + } + + private async initializeDataSource( + options: PostgresConnectionOptions + ): Promise { + const dataSource = new DataSource(options); + await dataSource.initialize(); + + this.logger.info("TypeORM data source initialized", { + database: String(dataSource.options.database), + schema: "schema" in dataSource.options ? dataSource.options.schema : undefined, + invalidWhereValuesBehavior: options.invalidWhereValuesBehavior + }); + + return dataSource; + } +} diff --git a/src/shared/infrastructure/persistence/typeorm/tenant-data-source.resolver.ts b/src/shared/infrastructure/persistence/typeorm/tenant-data-source.resolver.ts new file mode 100644 index 0000000..4660f8a --- /dev/null +++ b/src/shared/infrastructure/persistence/typeorm/tenant-data-source.resolver.ts @@ -0,0 +1,18 @@ +import { TenantContext } from "../../../domain/types/tenant-context"; +import { TypeOrmDataSourceFactory } from "./data-source.factory"; + +export class TenantDataSourceResolver { + constructor(private readonly dataSourceFactory: TypeOrmDataSourceFactory) {} + + public getPlatformDataSource() { + return this.dataSourceFactory.getPlatformDataSource(); + } + + public getDataSource(tenantContext?: TenantContext) { + if (!tenantContext) { + return this.dataSourceFactory.getPlatformDataSource(); + } + + return this.dataSourceFactory.getTenantDataSource(tenantContext); + } +} diff --git a/src/shared/infrastructure/security/bcrypt-password-hasher.ts b/src/shared/infrastructure/security/bcrypt-password-hasher.ts new file mode 100644 index 0000000..02d9504 --- /dev/null +++ b/src/shared/infrastructure/security/bcrypt-password-hasher.ts @@ -0,0 +1,15 @@ +import bcrypt from "bcrypt"; + +import { PasswordHasherPort } from "../../application/ports/password-hasher.port"; + +export class BcryptPasswordHasher implements PasswordHasherPort { + constructor(private readonly rounds: number) {} + + public async hash(plainText: string): Promise { + return bcrypt.hash(plainText, this.rounds); + } + + public async compare(plainText: string, hash: string): Promise { + return bcrypt.compare(plainText, hash); + } +} diff --git a/src/shared/infrastructure/security/jwt.service.ts b/src/shared/infrastructure/security/jwt.service.ts new file mode 100644 index 0000000..7aac0fa --- /dev/null +++ b/src/shared/infrastructure/security/jwt.service.ts @@ -0,0 +1,58 @@ +import jwt, { JwtPayload, SignOptions } from "jsonwebtoken"; + +import { AuthPrincipal } from "../../domain/types/request-context"; + +export class JwtService { + constructor( + private readonly options: { + secret: string; + expiresIn: string; + issuer: string; + audience: string; + } + ) {} + + public sign( + principal: AuthPrincipal, + options?: SignOptions + ): string { + return jwt.sign( + { + email: principal.email, + roles: principal.roles, + permissions: principal.permissions, + tenantId: principal.tenantId + }, + this.options.secret, + { + subject: principal.subject, + issuer: this.options.issuer, + audience: this.options.audience, + expiresIn: this.options.expiresIn as SignOptions["expiresIn"], + algorithm: "HS256", + ...options + } + ); + } + + public verify(token: string): AuthPrincipal { + const payload = jwt.verify(token, this.options.secret, { + issuer: this.options.issuer, + audience: this.options.audience, + algorithms: ["HS256"] + }) as JwtPayload; + + return { + subject: String(payload.sub ?? payload.subject ?? ""), + email: String(payload.email ?? ""), + roles: Array.isArray(payload.roles) + ? payload.roles.map((role) => String(role)) + : [], + permissions: Array.isArray(payload.permissions) + ? payload.permissions.map((permission) => String(permission)) + : [], + tenantId: + typeof payload.tenantId === "string" ? payload.tenantId : undefined + }; + } +} diff --git a/src/shared/infrastructure/system/node-crypto-id-generator.ts b/src/shared/infrastructure/system/node-crypto-id-generator.ts new file mode 100644 index 0000000..4d4a9d1 --- /dev/null +++ b/src/shared/infrastructure/system/node-crypto-id-generator.ts @@ -0,0 +1,9 @@ +import { randomUUID } from "crypto"; + +import { IdGeneratorPort } from "../../application/ports/id-generator.port"; + +export class NodeCryptoIdGenerator implements IdGeneratorPort { + public generate(): string { + return randomUUID(); + } +} diff --git a/src/shared/infrastructure/system/system-clock.ts b/src/shared/infrastructure/system/system-clock.ts new file mode 100644 index 0000000..c44797f --- /dev/null +++ b/src/shared/infrastructure/system/system-clock.ts @@ -0,0 +1,7 @@ +import { ClockPort } from "../../application/ports/clock.port"; + +export class SystemClock implements ClockPort { + public now(): Date { + return new Date(); + } +} diff --git a/src/shared/interfaces/http/middlewares/auth.middleware.ts b/src/shared/interfaces/http/middlewares/auth.middleware.ts new file mode 100644 index 0000000..95099a3 --- /dev/null +++ b/src/shared/interfaces/http/middlewares/auth.middleware.ts @@ -0,0 +1,30 @@ +import { NextFunction, Request, Response } from "express"; + +import { Logger } from "../../../infrastructure/logging/logger"; +import { JwtService } from "../../../infrastructure/security/jwt.service"; + +export function createAuthMiddleware( + jwtService: JwtService, + logger: Logger +) { + return (request: Request, _response: Response, next: NextFunction): void => { + const authorizationHeader = request.header("authorization"); + + if (!authorizationHeader?.startsWith("Bearer ")) { + next(); + return; + } + + try { + const token = authorizationHeader.replace("Bearer ", "").trim(); + request.principal = jwtService.verify(token); + } catch (error) { + request.principal = undefined; + logger.warn("Bearer token could not be verified", { + error: error instanceof Error ? error.message : "unknown" + }); + } + + next(); + }; +} diff --git a/src/shared/interfaces/http/middlewares/authorize.middleware.ts b/src/shared/interfaces/http/middlewares/authorize.middleware.ts new file mode 100644 index 0000000..0560a58 --- /dev/null +++ b/src/shared/interfaces/http/middlewares/authorize.middleware.ts @@ -0,0 +1,53 @@ +import { NextFunction, Request, Response } from "express"; + +interface AuthorizationOptions { + permissions?: string[]; + roles?: string[]; + requireAll?: boolean; +} + +export function authorize(options: AuthorizationOptions) { + return (request: Request, response: Response, next: NextFunction): void => { + const principal = request.principal; + + if (!principal) { + response.status(401).json({ + code: "UNAUTHENTICATED", + message: "Authentication is required to access this resource" + }); + return; + } + + const requiredPermissions = options.permissions ?? []; + const requiredRoles = options.roles ?? []; + const requireAll = options.requireAll ?? false; + + const hasPermissions = + requiredPermissions.length === 0 + ? true + : requireAll + ? requiredPermissions.every((permission) => + principal.permissions.includes(permission) + ) + : requiredPermissions.some((permission) => + principal.permissions.includes(permission) + ); + + const hasRoles = + requiredRoles.length === 0 + ? true + : requireAll + ? requiredRoles.every((role) => principal.roles.includes(role)) + : requiredRoles.some((role) => principal.roles.includes(role)); + + if (!hasPermissions || !hasRoles) { + response.status(403).json({ + code: "FORBIDDEN", + message: "You do not have permission to perform this action" + }); + return; + } + + next(); + }; +} diff --git a/src/shared/interfaces/http/middlewares/error.middleware.ts b/src/shared/interfaces/http/middlewares/error.middleware.ts new file mode 100644 index 0000000..999c3d3 --- /dev/null +++ b/src/shared/interfaces/http/middlewares/error.middleware.ts @@ -0,0 +1,46 @@ +import { NextFunction, Request, Response } from "express"; + +import { ValidateError } from "tsoa"; + +import { Logger } from "../../../infrastructure/logging/logger"; +import { presentError } from "../presenters/error.presenter"; + +export function notFoundMiddleware( + _request: Request, + response: Response, + _next: NextFunction +): void { + response.status(404).json({ + code: "NOT_FOUND", + message: "The requested resource does not exist" + }); +} + +export function errorMiddleware(logger: Logger) { + return ( + error: unknown, + request: Request, + response: Response, + _next: NextFunction + ): void => { + if (error instanceof ValidateError) { + response.status(422).json({ + code: "VALIDATION_ERROR", + message: "Request validation failed", + details: error.fields + }); + return; + } + + const { statusCode, body } = presentError(error); + + logger.error("HTTP request failed", { + path: request.path, + method: request.method, + requestId: request.requestContext?.requestId, + error: error instanceof Error ? error.message : "unknown" + }); + + response.status(statusCode).json(body); + }; +} diff --git a/src/shared/interfaces/http/middlewares/multer.middleware.ts b/src/shared/interfaces/http/middlewares/multer.middleware.ts new file mode 100644 index 0000000..5f1d596 --- /dev/null +++ b/src/shared/interfaces/http/middlewares/multer.middleware.ts @@ -0,0 +1,8 @@ +import multer from "multer"; + +export const multerMiddleware = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 10 * 1024 * 1024 // 10MB limit + } +}).single("file"); diff --git a/src/shared/interfaces/http/middlewares/request-context.middleware.ts b/src/shared/interfaces/http/middlewares/request-context.middleware.ts new file mode 100644 index 0000000..9426f2c --- /dev/null +++ b/src/shared/interfaces/http/middlewares/request-context.middleware.ts @@ -0,0 +1,20 @@ +import { randomUUID } from "crypto"; + +import { NextFunction, Request, Response } from "express"; + +import { RequestContext } from "../../../domain/types/request-context"; + +export function requestContextMiddleware( + request: Request, + _response: Response, + next: NextFunction +): void { + const requestContext: RequestContext = { + requestId: request.header("x-request-id") ?? randomUUID(), + tenant: request.tenantContext, + principal: request.principal + }; + + request.requestContext = requestContext; + next(); +} diff --git a/src/shared/interfaces/http/middlewares/require-auth.middleware.ts b/src/shared/interfaces/http/middlewares/require-auth.middleware.ts new file mode 100644 index 0000000..cce379b --- /dev/null +++ b/src/shared/interfaces/http/middlewares/require-auth.middleware.ts @@ -0,0 +1,17 @@ +import { NextFunction, Request, Response } from "express"; + +export function requireAuthentication( + request: Request, + response: Response, + next: NextFunction +): void { + if (!request.principal) { + response.status(401).json({ + code: "UNAUTHENTICATED", + message: "Authentication is required to access this resource" + }); + return; + } + + next(); +} diff --git a/src/shared/interfaces/http/middlewares/tenant.middleware.ts b/src/shared/interfaces/http/middlewares/tenant.middleware.ts new file mode 100644 index 0000000..32f2bc2 --- /dev/null +++ b/src/shared/interfaces/http/middlewares/tenant.middleware.ts @@ -0,0 +1,69 @@ +import { NextFunction, Request, Response } from "express"; + +import { ResolveTenantHandler } from "../../../../modules/tenancy/application/queries/resolve-tenant/resolve-tenant.handler"; +import { TenancyMode } from "../../../domain/types/tenant-context"; + +function resolveTenantFromHost(hostHeader?: string): string | undefined { + if (!hostHeader) { + return undefined; + } + + const host = hostHeader.split(":")[0]; + const parts = host.split("."); + + if (parts.length < 3) { + return undefined; + } + + return parts[0]; +} + +export function tenantMiddleware( + mode: TenancyMode, + resolveTenantHandler: ResolveTenantHandler +) { + return async ( + request: Request, + response: Response, + next: NextFunction + ): Promise => { + const hintedTenantId = request.header("x-tenant-id") ?? request.principal?.tenantId; + const hintedTenantSlug = + request.header("x-tenant-slug") ?? + resolveTenantFromHost(request.header("host")); + + if (!hintedTenantId && !hintedTenantSlug) { + next(); + return; + } + + try { + const tenantContext = await resolveTenantHandler.execute({ + tenantId: hintedTenantId ?? undefined, + tenantSlug: hintedTenantSlug ?? undefined, + mode + }); + + if (!tenantContext) { + next(); + return; + } + + if ( + request.principal?.tenantId && + request.principal.tenantId !== tenantContext.tenantId + ) { + response.status(403).json({ + code: "TENANT_CONTEXT_MISMATCH", + message: "Authenticated tenant does not match the requested tenant" + }); + return; + } + + request.tenantContext = tenantContext; + next(); + } catch (error) { + next(error); + } + }; +} diff --git a/src/shared/interfaces/http/presenters/error.presenter.ts b/src/shared/interfaces/http/presenters/error.presenter.ts new file mode 100644 index 0000000..60c3b6c --- /dev/null +++ b/src/shared/interfaces/http/presenters/error.presenter.ts @@ -0,0 +1,40 @@ +import { ApplicationError } from "../../../domain/errors/application-error"; +import { DomainError } from "../../../domain/errors/domain-error"; + +export interface ErrorResponseBody { + code: string; + message: string; +} + +export function presentError(error: unknown): { + statusCode: number; + body: ErrorResponseBody; +} { + if (error instanceof ApplicationError) { + return { + statusCode: error.statusCode, + body: { + code: error.code, + message: error.message + } + }; + } + + if (error instanceof DomainError) { + return { + statusCode: 400, + body: { + code: error.code, + message: error.message + } + }; + } + + return { + statusCode: 500, + body: { + code: "INTERNAL_SERVER_ERROR", + message: "An unexpected error occurred" + } + }; +} diff --git a/src/shared/interfaces/http/tsoa/authentication.ts b/src/shared/interfaces/http/tsoa/authentication.ts new file mode 100644 index 0000000..9750f48 --- /dev/null +++ b/src/shared/interfaces/http/tsoa/authentication.ts @@ -0,0 +1,32 @@ +import * as express from "express"; + +export function expressAuthentication( + request: express.Request, + securityName: string, + scopes?: string[] +): Promise { + if (securityName === "bearerAuth") { + // The request.principal is already set by the global authMiddleware in app.ts + // We just need to check if it exists and if the required scopes are present + const principal = (request as any).principal; + + if (!principal) { + return Promise.reject(new Error("Unauthorized")); + } + + if (scopes && scopes.length > 0) { + // In this application, permissions or roles might be used for scopes. + // E.g. in the token it's decoded as permissions. + const userScopes = principal.permissions || []; + const hasAllScopes = scopes.every((scope) => userScopes.includes(scope)); + + if (!hasAllScopes) { + return Promise.reject(new Error("Insufficient permissions")); + } + } + + return Promise.resolve(principal); + } + + return Promise.reject(new Error("Security schema not supported")); +} diff --git a/src/shared/interfaces/http/tsoa/generated/routes.ts b/src/shared/interfaces/http/tsoa/generated/routes.ts new file mode 100644 index 0000000..da6fda8 --- /dev/null +++ b/src/shared/interfaces/http/tsoa/generated/routes.ts @@ -0,0 +1,1474 @@ +/* tslint:disable */ +/* eslint-disable */ +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import type { TsoaRoute } from '@tsoa/runtime'; +import { fetchMiddlewares, ExpressTemplateService } from '@tsoa/runtime'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { TenantsController } from './../../../../../modules/tenancy/interfaces/http/controllers/tenants.controller'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { TasksController } from './../../../../../modules/task-management/interfaces/http/controllers/tasks.controller'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { UsersController } from './../../../../../modules/identity-access/interfaces/http/controllers/users.controller'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { AuthController } from './../../../../../modules/identity-access/interfaces/http/controllers/auth.controller'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { DocumentsController } from './../../../../../modules/document-management/interfaces/http/controllers/documents.controller'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { BillingController } from './../../../../../modules/billing/interfaces/http/controllers/billing.controller'; +import { expressAuthentication } from './../authentication'; +// @ts-ignore - no great way to install types from subpackage +import { iocContainer } from './../ioc'; +import type { IocContainer, IocContainerFactory } from '@tsoa/runtime'; +import type { Request as ExRequest, Response as ExResponse, RequestHandler, Router } from 'express'; +const multer = require('multer'); + + +const expressAuthenticationRecasted = expressAuthentication as (req: ExRequest, securityName: string, scopes?: string[], res?: ExResponse) => Promise; + + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + +const models: TsoaRoute.Models = { + "TenantStatus": { + "dataType": "refAlias", + "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["active"]},{"dataType":"enum","enums":["suspended"]}],"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "CompanyResponseDto": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "name": {"dataType":"string","required":true}, + "slug": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "TenantResponseDto": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "name": {"dataType":"string","required":true}, + "slug": {"dataType":"string","required":true}, + "status": {"ref":"TenantStatus","required":true}, + "company": {"ref":"CompanyResponseDto","required":true}, + "createdAt": {"dataType":"string","required":true}, + "updatedAt": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "OnboardTenantRequestDto": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string","required":true}, + "slug": {"dataType":"string","required":true}, + "companyName": {"dataType":"string"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "TaskStatus": { + "dataType": "refAlias", + "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["todo"]},{"dataType":"enum","enums":["in_progress"]},{"dataType":"enum","enums":["completed"]}],"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "TaskPriority": { + "dataType": "refAlias", + "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["low"]},{"dataType":"enum","enums":["medium"]},{"dataType":"enum","enums":["high"]},{"dataType":"enum","enums":["critical"]}],"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "SubTaskStatus": { + "dataType": "refAlias", + "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["todo"]},{"dataType":"enum","enums":["in_progress"]},{"dataType":"enum","enums":["completed"]}],"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "SubTaskResponseDto": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "title": {"dataType":"string","required":true}, + "description": {"dataType":"string"}, + "status": {"ref":"SubTaskStatus","required":true}, + "assignedUserId": {"dataType":"string"}, + "dueDate": {"dataType":"string"}, + "completedAt": {"dataType":"string"}, + "createdAt": {"dataType":"string","required":true}, + "updatedAt": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "TaskResponseDto": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "tenantId": {"dataType":"string","required":true}, + "title": {"dataType":"string","required":true}, + "description": {"dataType":"string"}, + "status": {"ref":"TaskStatus","required":true}, + "priority": {"ref":"TaskPriority","required":true}, + "dueDate": {"dataType":"string"}, + "assignedUserId": {"dataType":"string"}, + "completedAt": {"dataType":"string"}, + "createdAt": {"dataType":"string","required":true}, + "updatedAt": {"dataType":"string","required":true}, + "subtasks": {"dataType":"array","array":{"dataType":"refObject","ref":"SubTaskResponseDto"},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "CreateTaskRequestDto": { + "dataType": "refObject", + "properties": { + "tenantId": {"dataType":"string","required":true}, + "title": {"dataType":"string","required":true}, + "description": {"dataType":"string"}, + "priority": {"ref":"TaskPriority","required":true}, + "dueDate": {"dataType":"string"}, + "assignedUserId": {"dataType":"string"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "UpdateTaskRequestDto": { + "dataType": "refObject", + "properties": { + "tenantId": {"dataType":"string","required":true}, + "title": {"dataType":"string"}, + "description": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}]}, + "status": {"ref":"TaskStatus"}, + "priority": {"ref":"TaskPriority"}, + "dueDate": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}]}, + "assignedUserId": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}]}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "AddSubTaskRequestDto": { + "dataType": "refObject", + "properties": { + "tenantId": {"dataType":"string","required":true}, + "title": {"dataType":"string","required":true}, + "description": {"dataType":"string"}, + "assignedUserId": {"dataType":"string"}, + "dueDate": {"dataType":"string"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "UpdateSubTaskRequestDto": { + "dataType": "refObject", + "properties": { + "tenantId": {"dataType":"string","required":true}, + "title": {"dataType":"string"}, + "description": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}]}, + "status": {"ref":"SubTaskStatus"}, + "assignedUserId": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}]}, + "dueDate": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}]}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "UserResponseDto": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "tenantId": {"dataType":"string","required":true}, + "email": {"dataType":"string","required":true}, + "firstName": {"dataType":"string","required":true}, + "lastName": {"dataType":"string","required":true}, + "status": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["active"]},{"dataType":"enum","enums":["suspended"]},{"dataType":"enum","enums":["pending_invitation"]}],"required":true}, + "roles": {"dataType":"array","array":{"dataType":"string"},"required":true}, + "permissions": {"dataType":"array","array":{"dataType":"string"},"required":true}, + "createdAt": {"dataType":"string","required":true}, + "updatedAt": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "CreateUserRequestDto": { + "dataType": "refObject", + "properties": { + "tenantId": {"dataType":"string","required":true}, + "email": {"dataType":"string","required":true}, + "firstName": {"dataType":"string","required":true}, + "lastName": {"dataType":"string","required":true}, + "password": {"dataType":"string","required":true}, + "roleCodes": {"dataType":"array","array":{"dataType":"string"}}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "InviteUserRequestDto": { + "dataType": "refObject", + "properties": { + "tenantId": {"dataType":"string","required":true}, + "email": {"dataType":"string","required":true}, + "firstName": {"dataType":"string","required":true}, + "lastName": {"dataType":"string","required":true}, + "roleCodes": {"dataType":"array","array":{"dataType":"string"}}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "AssignRolesRequestDto": { + "dataType": "refObject", + "properties": { + "roleCodes": {"dataType":"array","array":{"dataType":"string"},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "UserStatus": { + "dataType": "refAlias", + "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["active"]},{"dataType":"enum","enums":["suspended"]},{"dataType":"enum","enums":["pending_invitation"]}],"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "UpdateUserStatusRequestDto": { + "dataType": "refObject", + "properties": { + "status": {"ref":"UserStatus","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "AuthResponseDto": { + "dataType": "refObject", + "properties": { + "accessToken": {"dataType":"string","required":true}, + "user": {"ref":"UserResponseDto","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "RegisterUserRequestDto": { + "dataType": "refObject", + "properties": { + "tenantId": {"dataType":"string","required":true}, + "email": {"dataType":"string","required":true}, + "firstName": {"dataType":"string","required":true}, + "lastName": {"dataType":"string","required":true}, + "password": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "LoginRequestDto": { + "dataType": "refObject", + "properties": { + "tenantId": {"dataType":"string","required":true}, + "email": {"dataType":"string","required":true}, + "password": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Record_string.string_": { + "dataType": "refAlias", + "type": {"dataType":"nestedObjectLiteral","nestedProperties":{},"additionalProperties":{"dataType":"string"},"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DocumentLinkTargetType": { + "dataType": "refAlias", + "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["task"]},{"dataType":"enum","enums":["subtask"]},{"dataType":"enum","enums":["user"]}],"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DocumentLinkResponseDto": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "targetType": {"ref":"DocumentLinkTargetType","required":true}, + "targetId": {"dataType":"string","required":true}, + "createdAt": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DocumentResponseDto": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "tenantId": {"dataType":"string","required":true}, + "title": {"dataType":"string","required":true}, + "description": {"dataType":"string"}, + "originalFileName": {"dataType":"string","required":true}, + "mimeType": {"dataType":"string","required":true}, + "sizeBytes": {"dataType":"double","required":true}, + "storageProvider": {"dataType":"string","required":true}, + "checksumSha256": {"dataType":"string","required":true}, + "uploadedByUserId": {"dataType":"string","required":true}, + "metadata": {"ref":"Record_string.string_","required":true}, + "tags": {"dataType":"array","array":{"dataType":"string"},"required":true}, + "links": {"dataType":"array","array":{"dataType":"refObject","ref":"DocumentLinkResponseDto"},"required":true}, + "createdAt": {"dataType":"string","required":true}, + "updatedAt": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DownloadDocumentResult": { + "dataType": "refObject", + "properties": { + "fileName": {"dataType":"string","required":true}, + "mimeType": {"dataType":"string","required":true}, + "sizeBytes": {"dataType":"double","required":true}, + "buffer": {"dataType":"buffer","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "UpdateDocumentRequestDto": { + "dataType": "refObject", + "properties": { + "tenantId": {"dataType":"string","required":true}, + "title": {"dataType":"string"}, + "description": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}]}, + "metadata": {"ref":"Record_string.string_"}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + "linkedTaskIds": {"dataType":"array","array":{"dataType":"string"}}, + "linkedSubTaskIds": {"dataType":"array","array":{"dataType":"string"}}, + "linkedUserIds": {"dataType":"array","array":{"dataType":"string"}}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "TenantSubscriptionResponseDto": { + "dataType": "refObject", + "properties": { + "planName": {"dataType":"string","required":true}, + "planCode": {"dataType":"string","required":true}, + "maxUsers": {"dataType":"double","required":true}, + "status": {"dataType":"string","required":true}, + "currentPeriodEnd": {"dataType":"string","required":true}, + "priceMonthly": {"dataType":"double","required":true}, + "currency": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ChangePlanRequestDto": { + "dataType": "refObject", + "properties": { + "planCode": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "BillingRecordResponseDto": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "amount": {"dataType":"double","required":true}, + "currency": {"dataType":"string","required":true}, + "status": {"dataType":"string","required":true}, + "paidAt": {"dataType":"string"}, + "invoiceUrl": {"dataType":"string"}, + "createdAt": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +}; +const templateService = new ExpressTemplateService(models, {"noImplicitAdditionalProperties":"throw-on-extras","bodyCoercion":true}); + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + + + +export function RegisterRoutes(app: Router,opts?:{multer?:ReturnType}) { + + // ########################################################################################################### + // NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look + // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa + // ########################################################################################################### + + const upload = opts?.multer || multer({"limits":{"fileSize":8388608}}); + + + const argsTenantsController_onboardTenant: Record = { + requestBody: {"in":"body","name":"requestBody","required":true,"ref":"OnboardTenantRequestDto"}, + }; + app.post('/api/v1/tenants', + ...(fetchMiddlewares(TenantsController)), + ...(fetchMiddlewares(TenantsController.prototype.onboardTenant)), + + async function TenantsController_onboardTenant(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsTenantsController_onboardTenant, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(TenantsController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'onboardTenant', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsTenantsController_getTenantById: Record = { + tenantId: {"in":"path","name":"tenantId","required":true,"dataType":"string"}, + }; + app.get('/api/v1/tenants/:tenantId', + ...(fetchMiddlewares(TenantsController)), + ...(fetchMiddlewares(TenantsController.prototype.getTenantById)), + + async function TenantsController_getTenantById(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsTenantsController_getTenantById, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(TenantsController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'getTenantById', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsTasksController_createTask: Record = { + requestBody: {"in":"body","name":"requestBody","required":true,"ref":"CreateTaskRequestDto"}, + }; + app.post('/api/v1/tasks', + authenticateMiddleware([{"bearerAuth":["tasks:create"]}]), + ...(fetchMiddlewares(TasksController)), + ...(fetchMiddlewares(TasksController.prototype.createTask)), + + async function TasksController_createTask(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsTasksController_createTask, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(TasksController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'createTask', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsTasksController_listTasks: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + status: {"in":"query","name":"status","ref":"TaskStatus"}, + assignedUserId: {"in":"query","name":"assignedUserId","dataType":"string"}, + }; + app.get('/api/v1/tasks', + authenticateMiddleware([{"bearerAuth":["tasks:read"]}]), + ...(fetchMiddlewares(TasksController)), + ...(fetchMiddlewares(TasksController.prototype.listTasks)), + + async function TasksController_listTasks(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsTasksController_listTasks, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(TasksController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'listTasks', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsTasksController_getTaskById: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + taskId: {"in":"path","name":"taskId","required":true,"dataType":"string"}, + }; + app.get('/api/v1/tasks/:taskId', + authenticateMiddleware([{"bearerAuth":["tasks:read"]}]), + ...(fetchMiddlewares(TasksController)), + ...(fetchMiddlewares(TasksController.prototype.getTaskById)), + + async function TasksController_getTaskById(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsTasksController_getTaskById, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(TasksController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'getTaskById', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsTasksController_updateTask: Record = { + taskId: {"in":"path","name":"taskId","required":true,"dataType":"string"}, + requestBody: {"in":"body","name":"requestBody","required":true,"ref":"UpdateTaskRequestDto"}, + }; + app.patch('/api/v1/tasks/:taskId', + authenticateMiddleware([{"bearerAuth":["tasks:update"]}]), + ...(fetchMiddlewares(TasksController)), + ...(fetchMiddlewares(TasksController.prototype.updateTask)), + + async function TasksController_updateTask(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsTasksController_updateTask, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(TasksController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'updateTask', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsTasksController_deleteTask: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + taskId: {"in":"path","name":"taskId","required":true,"dataType":"string"}, + }; + app.delete('/api/v1/tasks/:taskId', + authenticateMiddleware([{"bearerAuth":["tasks:delete"]}]), + ...(fetchMiddlewares(TasksController)), + ...(fetchMiddlewares(TasksController.prototype.deleteTask)), + + async function TasksController_deleteTask(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsTasksController_deleteTask, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(TasksController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'deleteTask', + controller, + response, + next, + validatedArgs, + successStatus: 204, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsTasksController_addSubTask: Record = { + taskId: {"in":"path","name":"taskId","required":true,"dataType":"string"}, + requestBody: {"in":"body","name":"requestBody","required":true,"ref":"AddSubTaskRequestDto"}, + }; + app.post('/api/v1/tasks/:taskId/subtasks', + authenticateMiddleware([{"bearerAuth":["tasks:update"]}]), + ...(fetchMiddlewares(TasksController)), + ...(fetchMiddlewares(TasksController.prototype.addSubTask)), + + async function TasksController_addSubTask(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsTasksController_addSubTask, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(TasksController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'addSubTask', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsTasksController_updateSubTask: Record = { + taskId: {"in":"path","name":"taskId","required":true,"dataType":"string"}, + subTaskId: {"in":"path","name":"subTaskId","required":true,"dataType":"string"}, + requestBody: {"in":"body","name":"requestBody","required":true,"ref":"UpdateSubTaskRequestDto"}, + }; + app.patch('/api/v1/tasks/:taskId/subtasks/:subTaskId', + authenticateMiddleware([{"bearerAuth":["tasks:update"]}]), + ...(fetchMiddlewares(TasksController)), + ...(fetchMiddlewares(TasksController.prototype.updateSubTask)), + + async function TasksController_updateSubTask(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsTasksController_updateSubTask, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(TasksController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'updateSubTask', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsTasksController_deleteSubTask: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + taskId: {"in":"path","name":"taskId","required":true,"dataType":"string"}, + subTaskId: {"in":"path","name":"subTaskId","required":true,"dataType":"string"}, + }; + app.delete('/api/v1/tasks/:taskId/subtasks/:subTaskId', + authenticateMiddleware([{"bearerAuth":["tasks:update"]}]), + ...(fetchMiddlewares(TasksController)), + ...(fetchMiddlewares(TasksController.prototype.deleteSubTask)), + + async function TasksController_deleteSubTask(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsTasksController_deleteSubTask, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(TasksController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'deleteSubTask', + controller, + response, + next, + validatedArgs, + successStatus: 204, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUsersController_createUser: Record = { + requestBody: {"in":"body","name":"requestBody","required":true,"ref":"CreateUserRequestDto"}, + }; + app.post('/api/v1/users', + authenticateMiddleware([{"bearerAuth":["users:create"]}]), + ...(fetchMiddlewares(UsersController)), + ...(fetchMiddlewares(UsersController.prototype.createUser)), + + async function UsersController_createUser(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUsersController_createUser, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(UsersController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'createUser', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUsersController_getUserById: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + userId: {"in":"path","name":"userId","required":true,"dataType":"string"}, + }; + app.get('/api/v1/users/:userId', + authenticateMiddleware([{"bearerAuth":["users:read"]}]), + ...(fetchMiddlewares(UsersController)), + ...(fetchMiddlewares(UsersController.prototype.getUserById)), + + async function UsersController_getUserById(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUsersController_getUserById, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(UsersController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'getUserById', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUsersController_inviteUser: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + requestBody: {"in":"body","name":"requestBody","required":true,"ref":"InviteUserRequestDto"}, + }; + app.post('/api/v1/users/invite', + authenticateMiddleware([{"bearerAuth":["users:create"]}]), + ...(fetchMiddlewares(UsersController)), + ...(fetchMiddlewares(UsersController.prototype.inviteUser)), + + async function UsersController_inviteUser(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUsersController_inviteUser, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(UsersController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'inviteUser', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUsersController_assignRoles: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + userId: {"in":"path","name":"userId","required":true,"dataType":"string"}, + requestBody: {"in":"body","name":"requestBody","required":true,"ref":"AssignRolesRequestDto"}, + }; + app.patch('/api/v1/users/:userId/roles', + authenticateMiddleware([{"bearerAuth":["users:manage-roles"]}]), + ...(fetchMiddlewares(UsersController)), + ...(fetchMiddlewares(UsersController.prototype.assignRoles)), + + async function UsersController_assignRoles(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUsersController_assignRoles, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(UsersController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'assignRoles', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUsersController_updateUserStatus: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + userId: {"in":"path","name":"userId","required":true,"dataType":"string"}, + requestBody: {"in":"body","name":"requestBody","required":true,"ref":"UpdateUserStatusRequestDto"}, + }; + app.patch('/api/v1/users/:userId/status', + authenticateMiddleware([{"bearerAuth":["users:manage"]}]), + ...(fetchMiddlewares(UsersController)), + ...(fetchMiddlewares(UsersController.prototype.updateUserStatus)), + + async function UsersController_updateUserStatus(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUsersController_updateUserStatus, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(UsersController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'updateUserStatus', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsAuthController_register: Record = { + requestBody: {"in":"body","name":"requestBody","required":true,"ref":"RegisterUserRequestDto"}, + }; + app.post('/api/v1/auth/register', + ...(fetchMiddlewares(AuthController)), + ...(fetchMiddlewares(AuthController.prototype.register)), + + async function AuthController_register(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_register, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(AuthController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'register', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsAuthController_login: Record = { + requestBody: {"in":"body","name":"requestBody","required":true,"ref":"LoginRequestDto"}, + }; + app.post('/api/v1/auth/login', + ...(fetchMiddlewares(AuthController)), + ...(fetchMiddlewares(AuthController.prototype.login)), + + async function AuthController_login(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_login, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(AuthController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'login', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsAuthController_me: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + app.get('/api/v1/auth/me', + authenticateMiddleware([{"bearerAuth":[]}]), + ...(fetchMiddlewares(AuthController)), + ...(fetchMiddlewares(AuthController.prototype.me)), + + async function AuthController_me(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_me, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(AuthController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'me', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsDocumentsController_uploadDocument: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + file: {"in":"formData","name":"file","required":true,"dataType":"file"}, + tenantId: {"in":"formData","name":"tenantId","required":true,"dataType":"string"}, + title: {"in":"formData","name":"title","dataType":"string"}, + description: {"in":"formData","name":"description","dataType":"string"}, + metadata: {"in":"formData","name":"metadata","dataType":"string"}, + tags: {"in":"formData","name":"tags","dataType":"string"}, + linkedTaskIds: {"in":"formData","name":"linkedTaskIds","dataType":"string"}, + linkedSubTaskIds: {"in":"formData","name":"linkedSubTaskIds","dataType":"string"}, + linkedUserIds: {"in":"formData","name":"linkedUserIds","dataType":"string"}, + }; + app.post('/api/v1/documents/upload', + authenticateMiddleware([{"bearerAuth":["documents:create"]}]), + upload.fields([ + { + name: "file", + maxCount: 1 + } + ]), + ...(fetchMiddlewares(DocumentsController)), + ...(fetchMiddlewares(DocumentsController.prototype.uploadDocument)), + + async function DocumentsController_uploadDocument(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsDocumentsController_uploadDocument, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(DocumentsController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'uploadDocument', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsDocumentsController_listDocuments: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + uploadedByUserId: {"in":"query","name":"uploadedByUserId","dataType":"string"}, + targetType: {"in":"query","name":"targetType","ref":"DocumentLinkTargetType"}, + targetId: {"in":"query","name":"targetId","dataType":"string"}, + }; + app.get('/api/v1/documents', + authenticateMiddleware([{"bearerAuth":["documents:read"]}]), + ...(fetchMiddlewares(DocumentsController)), + ...(fetchMiddlewares(DocumentsController.prototype.listDocuments)), + + async function DocumentsController_listDocuments(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsDocumentsController_listDocuments, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(DocumentsController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'listDocuments', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsDocumentsController_getDocumentById: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + documentId: {"in":"path","name":"documentId","required":true,"dataType":"string"}, + }; + app.get('/api/v1/documents/:documentId', + authenticateMiddleware([{"bearerAuth":["documents:read"]}]), + ...(fetchMiddlewares(DocumentsController)), + ...(fetchMiddlewares(DocumentsController.prototype.getDocumentById)), + + async function DocumentsController_getDocumentById(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsDocumentsController_getDocumentById, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(DocumentsController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'getDocumentById', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsDocumentsController_downloadDocument: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + documentId: {"in":"path","name":"documentId","required":true,"dataType":"string"}, + }; + app.get('/api/v1/documents/:documentId/download', + authenticateMiddleware([{"bearerAuth":["documents:read"]}]), + ...(fetchMiddlewares(DocumentsController)), + ...(fetchMiddlewares(DocumentsController.prototype.downloadDocument)), + + async function DocumentsController_downloadDocument(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsDocumentsController_downloadDocument, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(DocumentsController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'downloadDocument', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsDocumentsController_updateDocument: Record = { + documentId: {"in":"path","name":"documentId","required":true,"dataType":"string"}, + requestBody: {"in":"body","name":"requestBody","required":true,"ref":"UpdateDocumentRequestDto"}, + }; + app.patch('/api/v1/documents/:documentId', + authenticateMiddleware([{"bearerAuth":["documents:update"]}]), + ...(fetchMiddlewares(DocumentsController)), + ...(fetchMiddlewares(DocumentsController.prototype.updateDocument)), + + async function DocumentsController_updateDocument(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsDocumentsController_updateDocument, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(DocumentsController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'updateDocument', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsDocumentsController_deleteDocument: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + documentId: {"in":"path","name":"documentId","required":true,"dataType":"string"}, + }; + app.delete('/api/v1/documents/:documentId', + authenticateMiddleware([{"bearerAuth":["documents:delete"]}]), + ...(fetchMiddlewares(DocumentsController)), + ...(fetchMiddlewares(DocumentsController.prototype.deleteDocument)), + + async function DocumentsController_deleteDocument(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsDocumentsController_deleteDocument, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(DocumentsController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'deleteDocument', + controller, + response, + next, + validatedArgs, + successStatus: 204, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsBillingController_getSubscription: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + app.get('/api/v1/billing/subscription', + authenticateMiddleware([{"bearerAuth":[]}]), + ...(fetchMiddlewares(BillingController)), + ...(fetchMiddlewares(BillingController.prototype.getSubscription)), + + async function BillingController_getSubscription(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsBillingController_getSubscription, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(BillingController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'getSubscription', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsBillingController_changePlan: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + body: {"in":"body","name":"body","required":true,"ref":"ChangePlanRequestDto"}, + }; + app.patch('/api/v1/billing/subscription/plan', + authenticateMiddleware([{"bearerAuth":[]}]), + ...(fetchMiddlewares(BillingController)), + ...(fetchMiddlewares(BillingController.prototype.changePlan)), + + async function BillingController_changePlan(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsBillingController_changePlan, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(BillingController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'changePlan', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsBillingController_getHistory: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + app.get('/api/v1/billing/history', + authenticateMiddleware([{"bearerAuth":[]}]), + ...(fetchMiddlewares(BillingController)), + ...(fetchMiddlewares(BillingController.prototype.getHistory)), + + async function BillingController_getHistory(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsBillingController_getHistory, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(BillingController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'getHistory', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + function authenticateMiddleware(security: TsoaRoute.Security[] = []) { + return async function runAuthenticationMiddleware(request: any, response: any, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + // keep track of failed auth attempts so we can hand back the most + // recent one. This behavior was previously existing so preserving it + // here + const failedAttempts: any[] = []; + const pushAndRethrow = (error: any) => { + failedAttempts.push(error); + throw error; + }; + + const secMethodOrPromises: Promise[] = []; + for (const secMethod of security) { + if (Object.keys(secMethod).length > 1) { + const secMethodAndPromises: Promise[] = []; + + for (const name in secMethod) { + secMethodAndPromises.push( + expressAuthenticationRecasted(request, name, secMethod[name], response) + .catch(pushAndRethrow) + ); + } + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + secMethodOrPromises.push(Promise.all(secMethodAndPromises) + .then(users => { return users[0]; })); + } else { + for (const name in secMethod) { + secMethodOrPromises.push( + expressAuthenticationRecasted(request, name, secMethod[name], response) + .catch(pushAndRethrow) + ); + } + } + } + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + try { + request['user'] = await Promise.any(secMethodOrPromises); + + // Response was sent in middleware, abort + if (response.writableEnded) { + return; + } + + next(); + } + catch(err) { + // Show most recent error as response + const error = failedAttempts.pop(); + error.status = error.status || 401; + + // Response was sent in middleware, abort + if (response.writableEnded) { + return; + } + next(error); + } + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + } + } + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +} + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa diff --git a/src/shared/interfaces/http/tsoa/generated/swagger.json b/src/shared/interfaces/http/tsoa/generated/swagger.json new file mode 100644 index 0000000..fec133f --- /dev/null +++ b/src/shared/interfaces/http/tsoa/generated/swagger.json @@ -0,0 +1,1912 @@ +{ + "openapi": "3.0.0", + "components": { + "examples": {}, + "headers": {}, + "parameters": {}, + "requestBodies": {}, + "responses": {}, + "schemas": { + "TenantStatus": { + "type": "string", + "enum": [ + "active", + "suspended" + ] + }, + "CompanyResponseDto": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "slug" + ], + "type": "object", + "additionalProperties": false + }, + "TenantResponseDto": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/TenantStatus" + }, + "company": { + "$ref": "#/components/schemas/CompanyResponseDto" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "slug", + "status", + "company", + "createdAt", + "updatedAt" + ], + "type": "object", + "additionalProperties": false + }, + "OnboardTenantRequestDto": { + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "companyName": { + "type": "string" + } + }, + "required": [ + "name", + "slug" + ], + "type": "object", + "additionalProperties": false + }, + "TaskStatus": { + "type": "string", + "enum": [ + "todo", + "in_progress", + "completed" + ] + }, + "TaskPriority": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "critical" + ] + }, + "SubTaskStatus": { + "type": "string", + "enum": [ + "todo", + "in_progress", + "completed" + ] + }, + "SubTaskResponseDto": { + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/SubTaskStatus" + }, + "assignedUserId": { + "type": "string" + }, + "dueDate": { + "type": "string" + }, + "completedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": [ + "id", + "title", + "status", + "createdAt", + "updatedAt" + ], + "type": "object", + "additionalProperties": false + }, + "TaskResponseDto": { + "properties": { + "id": { + "type": "string" + }, + "tenantId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/TaskStatus" + }, + "priority": { + "$ref": "#/components/schemas/TaskPriority" + }, + "dueDate": { + "type": "string" + }, + "assignedUserId": { + "type": "string" + }, + "completedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "subtasks": { + "items": { + "$ref": "#/components/schemas/SubTaskResponseDto" + }, + "type": "array" + } + }, + "required": [ + "id", + "tenantId", + "title", + "status", + "priority", + "createdAt", + "updatedAt", + "subtasks" + ], + "type": "object", + "additionalProperties": false + }, + "CreateTaskRequestDto": { + "properties": { + "tenantId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "priority": { + "$ref": "#/components/schemas/TaskPriority" + }, + "dueDate": { + "type": "string" + }, + "assignedUserId": { + "type": "string" + } + }, + "required": [ + "tenantId", + "title", + "priority" + ], + "type": "object", + "additionalProperties": false + }, + "UpdateTaskRequestDto": { + "properties": { + "tenantId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/TaskStatus" + }, + "priority": { + "$ref": "#/components/schemas/TaskPriority" + }, + "dueDate": { + "type": "string", + "nullable": true + }, + "assignedUserId": { + "type": "string", + "nullable": true + } + }, + "required": [ + "tenantId" + ], + "type": "object", + "additionalProperties": false + }, + "AddSubTaskRequestDto": { + "properties": { + "tenantId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "assignedUserId": { + "type": "string" + }, + "dueDate": { + "type": "string" + } + }, + "required": [ + "tenantId", + "title" + ], + "type": "object", + "additionalProperties": false + }, + "UpdateSubTaskRequestDto": { + "properties": { + "tenantId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/SubTaskStatus" + }, + "assignedUserId": { + "type": "string", + "nullable": true + }, + "dueDate": { + "type": "string", + "nullable": true + } + }, + "required": [ + "tenantId" + ], + "type": "object", + "additionalProperties": false + }, + "UserResponseDto": { + "properties": { + "id": { + "type": "string" + }, + "tenantId": { + "type": "string" + }, + "email": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "suspended", + "pending_invitation" + ] + }, + "roles": { + "items": { + "type": "string" + }, + "type": "array" + }, + "permissions": { + "items": { + "type": "string" + }, + "type": "array" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": [ + "id", + "tenantId", + "email", + "firstName", + "lastName", + "status", + "roles", + "permissions", + "createdAt", + "updatedAt" + ], + "type": "object", + "additionalProperties": false + }, + "CreateUserRequestDto": { + "properties": { + "tenantId": { + "type": "string" + }, + "email": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "password": { + "type": "string" + }, + "roleCodes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "tenantId", + "email", + "firstName", + "lastName", + "password" + ], + "type": "object", + "additionalProperties": false + }, + "InviteUserRequestDto": { + "properties": { + "tenantId": { + "type": "string", + "format": "uuid" + }, + "email": { + "type": "string", + "format": "email" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "roleCodes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "tenantId", + "email", + "firstName", + "lastName" + ], + "type": "object", + "additionalProperties": false + }, + "AssignRolesRequestDto": { + "properties": { + "roleCodes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "roleCodes" + ], + "type": "object", + "additionalProperties": false + }, + "UserStatus": { + "type": "string", + "enum": [ + "active", + "suspended", + "pending_invitation" + ] + }, + "UpdateUserStatusRequestDto": { + "properties": { + "status": { + "$ref": "#/components/schemas/UserStatus" + } + }, + "required": [ + "status" + ], + "type": "object", + "additionalProperties": false + }, + "AuthResponseDto": { + "properties": { + "accessToken": { + "type": "string" + }, + "user": { + "$ref": "#/components/schemas/UserResponseDto" + } + }, + "required": [ + "accessToken", + "user" + ], + "type": "object", + "additionalProperties": false + }, + "RegisterUserRequestDto": { + "properties": { + "tenantId": { + "type": "string" + }, + "email": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": [ + "tenantId", + "email", + "firstName", + "lastName", + "password" + ], + "type": "object", + "additionalProperties": false + }, + "LoginRequestDto": { + "properties": { + "tenantId": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": [ + "tenantId", + "email", + "password" + ], + "type": "object", + "additionalProperties": false + }, + "Record_string.string_": { + "properties": {}, + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Construct a type with a set of properties K of type T" + }, + "DocumentLinkTargetType": { + "type": "string", + "enum": [ + "task", + "subtask", + "user" + ] + }, + "DocumentLinkResponseDto": { + "properties": { + "id": { + "type": "string" + }, + "targetType": { + "$ref": "#/components/schemas/DocumentLinkTargetType" + }, + "targetId": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "required": [ + "id", + "targetType", + "targetId", + "createdAt" + ], + "type": "object", + "additionalProperties": false + }, + "DocumentResponseDto": { + "properties": { + "id": { + "type": "string" + }, + "tenantId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "originalFileName": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "sizeBytes": { + "type": "number", + "format": "double" + }, + "storageProvider": { + "type": "string" + }, + "checksumSha256": { + "type": "string" + }, + "uploadedByUserId": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Record_string.string_" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "links": { + "items": { + "$ref": "#/components/schemas/DocumentLinkResponseDto" + }, + "type": "array" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": [ + "id", + "tenantId", + "title", + "originalFileName", + "mimeType", + "sizeBytes", + "storageProvider", + "checksumSha256", + "uploadedByUserId", + "metadata", + "tags", + "links", + "createdAt", + "updatedAt" + ], + "type": "object", + "additionalProperties": false + }, + "DownloadDocumentResult": { + "properties": { + "fileName": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "sizeBytes": { + "type": "number", + "format": "double" + }, + "buffer": { + "type": "string", + "format": "byte" + } + }, + "required": [ + "fileName", + "mimeType", + "sizeBytes", + "buffer" + ], + "type": "object", + "additionalProperties": false + }, + "UpdateDocumentRequestDto": { + "properties": { + "tenantId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "metadata": { + "$ref": "#/components/schemas/Record_string.string_" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "linkedTaskIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "linkedSubTaskIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "linkedUserIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "tenantId" + ], + "type": "object", + "additionalProperties": false + }, + "TenantSubscriptionResponseDto": { + "properties": { + "planName": { + "type": "string" + }, + "planCode": { + "type": "string" + }, + "maxUsers": { + "type": "number", + "format": "double" + }, + "status": { + "type": "string" + }, + "currentPeriodEnd": { + "type": "string" + }, + "priceMonthly": { + "type": "number", + "format": "double" + }, + "currency": { + "type": "string" + } + }, + "required": [ + "planName", + "planCode", + "maxUsers", + "status", + "currentPeriodEnd", + "priceMonthly", + "currency" + ], + "type": "object", + "additionalProperties": false + }, + "ChangePlanRequestDto": { + "properties": { + "planCode": { + "type": "string" + } + }, + "required": [ + "planCode" + ], + "type": "object", + "additionalProperties": false + }, + "BillingRecordResponseDto": { + "properties": { + "id": { + "type": "string" + }, + "amount": { + "type": "number", + "format": "double" + }, + "currency": { + "type": "string" + }, + "status": { + "type": "string" + }, + "paidAt": { + "type": "string" + }, + "invoiceUrl": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "required": [ + "id", + "amount", + "currency", + "status", + "createdAt" + ], + "type": "object", + "additionalProperties": false + } + }, + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + } + }, + "info": { + "title": "gem-track-backend", + "version": "1.0.0", + "description": "Industrial-grade backend starter using Node.js, TypeScript, TypeORM, TSOA, and Swagger.", + "contact": {} + }, + "paths": { + "/v1/tenants": { + "post": { + "operationId": "OnboardTenant", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TenantResponseDto" + } + } + } + }, + "409": { + "description": "Tenant slug already exists" + } + }, + "tags": [ + "Tenants" + ], + "security": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OnboardTenantRequestDto" + } + } + } + } + } + }, + "/v1/tenants/{tenantId}": { + "get": { + "operationId": "GetTenantById", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TenantResponseDto" + } + } + } + }, + "404": { + "description": "Tenant not found" + } + }, + "tags": [ + "Tenants" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "tenantId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/v1/tasks": { + "post": { + "operationId": "CreateTask", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskResponseDto" + } + } + } + } + }, + "tags": [ + "Tasks" + ], + "security": [ + { + "bearerAuth": [ + "tasks:create" + ] + } + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTaskRequestDto" + } + } + } + } + }, + "get": { + "operationId": "ListTasks", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/TaskResponseDto" + }, + "type": "array" + } + } + } + } + }, + "tags": [ + "Tasks" + ], + "security": [ + { + "bearerAuth": [ + "tasks:read" + ] + } + ], + "parameters": [ + { + "in": "query", + "name": "status", + "required": false, + "schema": { + "$ref": "#/components/schemas/TaskStatus" + } + }, + { + "in": "query", + "name": "assignedUserId", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/v1/tasks/{taskId}": { + "get": { + "operationId": "GetTaskById", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskResponseDto" + } + } + } + }, + "404": { + "description": "Task not found" + } + }, + "tags": [ + "Tasks" + ], + "security": [ + { + "bearerAuth": [ + "tasks:read" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "taskId", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "patch": { + "operationId": "UpdateTask", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskResponseDto" + } + } + } + } + }, + "tags": [ + "Tasks" + ], + "security": [ + { + "bearerAuth": [ + "tasks:update" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "taskId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTaskRequestDto" + } + } + } + } + }, + "delete": { + "operationId": "DeleteTask", + "responses": { + "204": { + "description": "Deleted" + } + }, + "tags": [ + "Tasks" + ], + "security": [ + { + "bearerAuth": [ + "tasks:delete" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "taskId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/v1/tasks/{taskId}/subtasks": { + "post": { + "operationId": "AddSubTask", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskResponseDto" + } + } + } + } + }, + "tags": [ + "Tasks" + ], + "security": [ + { + "bearerAuth": [ + "tasks:update" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "taskId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddSubTaskRequestDto" + } + } + } + } + } + }, + "/v1/tasks/{taskId}/subtasks/{subTaskId}": { + "patch": { + "operationId": "UpdateSubTask", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskResponseDto" + } + } + } + } + }, + "tags": [ + "Tasks" + ], + "security": [ + { + "bearerAuth": [ + "tasks:update" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "taskId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "subTaskId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSubTaskRequestDto" + } + } + } + } + }, + "delete": { + "operationId": "DeleteSubTask", + "responses": { + "204": { + "description": "Deleted" + } + }, + "tags": [ + "Tasks" + ], + "security": [ + { + "bearerAuth": [ + "tasks:update" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "taskId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "subTaskId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/v1/users": { + "post": { + "operationId": "CreateUser", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponseDto" + } + } + } + } + }, + "tags": [ + "Users" + ], + "security": [ + { + "bearerAuth": [ + "users:create" + ] + } + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserRequestDto" + } + } + } + } + } + }, + "/v1/users/{userId}": { + "get": { + "operationId": "GetUserById", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponseDto" + } + } + } + }, + "404": { + "description": "User not found" + } + }, + "tags": [ + "Users" + ], + "security": [ + { + "bearerAuth": [ + "users:read" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "userId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/v1/users/invite": { + "post": { + "operationId": "InviteUser", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponseDto" + } + } + } + } + }, + "tags": [ + "Users" + ], + "security": [ + { + "bearerAuth": [ + "users:create" + ] + } + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InviteUserRequestDto" + } + } + } + } + } + }, + "/v1/users/{userId}/roles": { + "patch": { + "operationId": "AssignRoles", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponseDto" + } + } + } + } + }, + "tags": [ + "Users" + ], + "security": [ + { + "bearerAuth": [ + "users:manage-roles" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "userId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignRolesRequestDto" + } + } + } + } + } + }, + "/v1/users/{userId}/status": { + "patch": { + "operationId": "UpdateUserStatus", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponseDto" + } + } + } + } + }, + "tags": [ + "Users" + ], + "security": [ + { + "bearerAuth": [ + "users:manage" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "userId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserStatusRequestDto" + } + } + } + } + } + }, + "/v1/auth/register": { + "post": { + "operationId": "Register", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthResponseDto" + } + } + } + } + }, + "tags": [ + "Auth" + ], + "security": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterUserRequestDto" + } + } + } + } + } + }, + "/v1/auth/login": { + "post": { + "operationId": "Login", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthResponseDto" + } + } + } + } + }, + "tags": [ + "Auth" + ], + "security": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequestDto" + } + } + } + } + } + }, + "/v1/auth/me": { + "get": { + "operationId": "Me", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponseDto" + } + } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "tags": [ + "Auth" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [] + } + }, + "/v1/documents/upload": { + "post": { + "operationId": "UploadDocument", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentResponseDto" + } + } + } + } + }, + "tags": [ + "Documents" + ], + "security": [ + { + "bearerAuth": [ + "documents:create" + ] + } + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "tenantId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "metadata": { + "type": "string" + }, + "tags": { + "type": "string" + }, + "linkedTaskIds": { + "type": "string" + }, + "linkedSubTaskIds": { + "type": "string" + }, + "linkedUserIds": { + "type": "string" + } + }, + "required": [ + "file", + "tenantId" + ] + } + } + } + } + } + }, + "/v1/documents": { + "get": { + "operationId": "ListDocuments", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/DocumentResponseDto" + }, + "type": "array" + } + } + } + } + }, + "tags": [ + "Documents" + ], + "security": [ + { + "bearerAuth": [ + "documents:read" + ] + } + ], + "parameters": [ + { + "in": "query", + "name": "uploadedByUserId", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "targetType", + "required": false, + "schema": { + "$ref": "#/components/schemas/DocumentLinkTargetType" + } + }, + { + "in": "query", + "name": "targetId", + "required": false, + "schema": { + "type": "string" + } + } + ] + } + }, + "/v1/documents/{documentId}": { + "get": { + "operationId": "GetDocumentById", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentResponseDto" + } + } + } + } + }, + "tags": [ + "Documents" + ], + "security": [ + { + "bearerAuth": [ + "documents:read" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "documentId", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "patch": { + "operationId": "UpdateDocument", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentResponseDto" + } + } + } + } + }, + "tags": [ + "Documents" + ], + "security": [ + { + "bearerAuth": [ + "documents:update" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "documentId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateDocumentRequestDto" + } + } + } + } + }, + "delete": { + "operationId": "DeleteDocument", + "responses": { + "204": { + "description": "Deleted" + } + }, + "tags": [ + "Documents" + ], + "security": [ + { + "bearerAuth": [ + "documents:delete" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "documentId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/v1/documents/{documentId}/download": { + "get": { + "operationId": "DownloadDocument", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadDocumentResult" + } + } + } + } + }, + "tags": [ + "Documents" + ], + "security": [ + { + "bearerAuth": [ + "documents:read" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "documentId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/v1/billing/subscription": { + "get": { + "operationId": "GetSubscription", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TenantSubscriptionResponseDto" + } + } + } + } + }, + "tags": [ + "Billing" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [] + } + }, + "/v1/billing/subscription/plan": { + "patch": { + "operationId": "ChangePlan", + "responses": { + "204": { + "description": "No content" + } + }, + "tags": [ + "Billing" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangePlanRequestDto" + } + } + } + } + } + }, + "/v1/billing/history": { + "get": { + "operationId": "GetHistory", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/BillingRecordResponseDto" + }, + "type": "array" + } + } + } + } + }, + "tags": [ + "Billing" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [] + } + } + }, + "servers": [ + { + "url": "/api" + } + ] +} \ No newline at end of file diff --git a/src/shared/interfaces/http/tsoa/ioc.ts b/src/shared/interfaces/http/tsoa/ioc.ts new file mode 100644 index 0000000..379ecee --- /dev/null +++ b/src/shared/interfaces/http/tsoa/ioc.ts @@ -0,0 +1,67 @@ +import { IocContainer } from "tsoa"; +import { getApplicationContext } from "../../../../bootstrap/application-context"; + +export const iocContainer: IocContainer = { + get: (controller: any): T => { + const context = getApplicationContext(); + + if (controller.name === "DocumentsController") { + return new (controller as any)( + context.useCases.documentManagement.uploadDocument, + context.useCases.documentManagement.updateDocument, + context.useCases.documentManagement.deleteDocument, + context.useCases.documentManagement.getDocumentById, + context.useCases.documentManagement.listDocuments, + context.useCases.documentManagement.downloadDocument + ) as T; + } + + if (controller.name === "BillingController") { + return new (controller as any)( + context.useCases.billing.getTenantSubscription, + context.useCases.billing.changeSubscriptionPlan, + context.useCases.billing.getBillingHistory + ) as T; + } + + if (controller.name === "AuthController") { + return new (controller as any)( + context.useCases.identityAccess.registerUser, + context.useCases.identityAccess.login, + context.useCases.identityAccess.getCurrentUser + ) as T; + } + + if (controller.name === "UsersController") { + return new (controller as any)( + context.useCases.identityAccess.createUser, + context.useCases.identityAccess.inviteUser, + context.useCases.identityAccess.assignRoles, + context.useCases.identityAccess.updateUserStatus, + context.useCases.identityAccess.getUserById + ) as T; + } + + if (controller.name === "TasksController") { + return new (controller as any)( + context.useCases.taskManagement.createTask, + context.useCases.taskManagement.updateTask, + context.useCases.taskManagement.deleteTask, + context.useCases.taskManagement.addSubTask, + context.useCases.taskManagement.updateSubTask, + context.useCases.taskManagement.deleteSubTask, + context.useCases.taskManagement.getTaskById, + context.useCases.taskManagement.listTasks + ) as T; + } + + if (controller.name === "TenantsController") { + return new (controller as any)( + context.useCases.tenancy.onboardTenant, + context.useCases.tenancy.getTenantById + ) as T; + } + + throw new Error(`Controller ${controller.name} not found in IOC container`); + } +}; diff --git a/src/shared/interfaces/http/tsoa/swagger.ts b/src/shared/interfaces/http/tsoa/swagger.ts new file mode 100644 index 0000000..e6eef38 --- /dev/null +++ b/src/shared/interfaces/http/tsoa/swagger.ts @@ -0,0 +1,48 @@ +import fs from "fs"; +import path from "path"; + +import { Router } from "express"; +import swaggerUi from "swagger-ui-express"; + +const generatedSwaggerPath = path.resolve( + process.cwd(), + "src/shared/interfaces/http/tsoa/generated/swagger.json" +); + +export function loadSwaggerDocument(): Record { + if (fs.existsSync(generatedSwaggerPath)) { + return JSON.parse(fs.readFileSync(generatedSwaggerPath, "utf-8")) as Record< + string, + unknown + >; + } + + return { + openapi: "3.0.0", + info: { + title: "Gem Track Backend API", + version: "1.0.0" + } + }; +} + +export function createSwaggerRouter(swaggerUrl: string): Router { + const router = Router(); + + router.use( + "/", + swaggerUi.serveFiles(undefined, { + swaggerOptions: { + url: swaggerUrl + } + }), + swaggerUi.setup(undefined, { + explorer: true, + swaggerOptions: { + url: swaggerUrl + } + }) + ); + + return router; +} diff --git a/src/types/express.d.ts b/src/types/express.d.ts new file mode 100644 index 0000000..a5a8ff4 --- /dev/null +++ b/src/types/express.d.ts @@ -0,0 +1,14 @@ +import { AuthPrincipal, RequestContext } from "../shared/domain/types/request-context"; +import { TenantContext } from "../shared/domain/types/tenant-context"; + +declare global { + namespace Express { + interface Request { + principal?: AuthPrincipal; + tenantContext?: TenantContext; + requestContext?: RequestContext; + } + } +} + +export {}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..011368a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "node16", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "rootDir": "src", + "outDir": "dist", + "sourceMap": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "types": ["node"] + }, + "include": ["src/**/*.ts", "src/**/*.d.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/tsoa.json b/tsoa.json new file mode 100644 index 0000000..8754706 --- /dev/null +++ b/tsoa.json @@ -0,0 +1,26 @@ +{ + "entryFile": "src/main.ts", + "noImplicitAdditionalProperties": "throw-on-extras", + "controllerPathGlobs": [ + "src/modules/**/interfaces/http/controllers/*.ts" + ], + "spec": { + "outputDirectory": "src/shared/interfaces/http/tsoa/generated", + "specVersion": 3, + "basePath": "/api", + "securityDefinitions": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + } + }, + "routes": { + "routesDir": "src/shared/interfaces/http/tsoa/generated", + "middleware": "express", + "basePath": "/api", + "iocModule": "src/shared/interfaces/http/tsoa/ioc.ts", + "authenticationModule": "src/shared/interfaces/http/tsoa/authentication.ts" + } +}