From d844f242def6e6d62b4732cc5956f8dc10ad1825 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 27 Apr 2025 22:47:47 +0200 Subject: [PATCH] Inicial --- .dockerignore | 1 + .eslintrc.js | 5 + .gitignore | 36 + .npmrc | 1 + .prettierrc | 13 + .vscode/launch.json | 60 + .vscode/settings.json | 30 + README.md | 75 + apps/server/.env | 11 + apps/server/.env.development | 11 + apps/server/Dockerfile | 47 + apps/server/eslint.config.js | 10 + apps/server/package.json | 77 + apps/server/src/__tests__/server.test.ts | 23 + apps/server/src/app.ts | 56 + apps/server/src/config/database.ts | 48 + apps/server/src/config/index.ts | 12 + apps/server/src/config/register-models.ts | 66 + apps/server/src/core/helpers/index.ts | 2 + apps/server/src/core/helpers/model-loader.ts | 54 + apps/server/src/core/helpers/module-loader.ts | 55 + .../src/core/helpers/service-registry.ts | 36 + apps/server/src/index.ts | 137 + apps/server/src/modules.ts | 9 + apps/server/tsconfig.json | 16 + apps/web/.eslintrc.cjs | 5 + apps/web/index.html | 13 + apps/web/package.json | 27 + apps/web/public/typescript.svg | 1 + apps/web/public/vite.svg | 1 + apps/web/src/main.d.ts | 2 + apps/web/src/main.d.ts.map | 1 + apps/web/src/main.js | 7 + apps/web/src/main.tsx | 25 + apps/web/src/style.css | 97 + apps/web/src/vite-env.d.ts | 1 + apps/web/tsconfig.json | 7 + apps/web/vite.config.ts | 6 + docker-compose.yml | 29 + docs/API.md | 422 + docs/DsRegistroVeriFactu.xlsx | Bin 0 -> 59877 bytes docs/README.md | 84 + docs/REQUISITOS CLIENTES.md | 165 + docs/REQUISITOS GENERALES.md | 207 + modules/invoices/package.json | 81 + modules/invoices/src/client/index.ts | 1 + modules/invoices/src/index.ts | 2 + .../application/create-invoice.use-case.ts | 141 + .../application/delete-invoice.use-case.ts | 23 + .../application/get-invoice.use-case.ts | 23 + .../invoices/src/server/application/index.ts | 5 + .../application/list-invoices.use-case.ts | 22 + .../src/server/application/services/index.ts | 0 .../services/participantAddressFinder.ts.bak | 70 + .../services/participantFinder.ts.bak | 20 + .../application/update-invoice.use-case.bak | 400 + .../src/server/domain/aggregates/index.ts | 1 + .../src/server/domain/aggregates/invoice.ts | 203 + .../src/server/domain/entities/index.ts | 2 + .../domain/entities/invoice-customer/index.ts | 2 + .../invoice-customer/invoice-address.ts | 78 + .../invoice-customer/invoice-customer.ts | 61 + .../__tests__/invoice-item.test.ts | 84 + .../domain/entities/invoice-items/index.ts | 2 + .../entities/invoice-items/invoice-item.ts | 94 + .../entities/invoice-items/invoice-items.ts | 8 + modules/invoices/src/server/domain/index.ts | 5 + .../src/server/domain/repositories/index.ts | 1 + .../invoice-repository.interface.ts | 12 + .../src/server/domain/services/index.ts | 2 + .../services/invoice-service.interface.ts | 22 + .../server/domain/services/invoice.service.ts | 82 + .../src/server/domain/value-objects/index.ts | 5 + .../value-objects/invoice-address-type.ts | 38 + .../value-objects/invoice-item-description.ts | 50 + .../domain/value-objects/invoice-number.ts | 42 + .../domain/value-objects/invoice-serie.ts | 50 + .../domain/value-objects/invoice-status.ts | 80 + modules/invoices/src/server/index.ts | 28 + .../server/intrastructure/express/index.ts | 1 + .../intrastructure/express/invoices.routes.ts | 66 + .../src/server/intrastructure/index.ts | 3 + .../mappers/contact.mapper.ts.bak | 63 + .../mappers/contactAddress.mapper.ts.bak | 65 + .../server/intrastructure/mappers/index.ts | 1 + .../mappers/invoice-item.mapper.ts | 100 + .../intrastructure/mappers/invoice.mapper.ts | 97 + .../mappers/invoiceParticipant.mapper.ts.bak | 119 + .../invoiceParticipantAddress.mapper.ts.bak | 87 + .../sequelize/contact.mo.del.ts.bak | 84 + .../sequelize/contactAddress.mo.del.ts.bak | 75 + .../server/intrastructure/sequelize/index.ts | 11 + .../sequelize/invoice-item.model.ts | 166 + .../intrastructure/sequelize/invoice.model.ts | 141 + .../sequelize/invoice.repository.ts | 111 + .../invoiceParticipant.mo.del.ts.bak | 106 + .../invoiceParticipantAddress.mo.del.ts.bak | 94 + .../create-invoice.controller.ts | 45 + .../controllers/create-invoice/index.ts | 17 + .../presenter/InvoiceItem.presenter.ts.bak | 19 + .../InvoiceParticipant.presenter.ts.bak | 26 + ...InvoiceParticipantAddress.presenter.ts.bak | 19 + .../presenter/create-invoice.presenter.ts | 28 + .../create-invoice/presenter/index.ts | 1 + .../delete-invoice.controller.ts.bak | 12 + .../controllers/delete-invoice/index.ts.bak | 14 + .../get-invoice/get-invoice.controller.ts | 44 + .../controllers/get-invoice/index.ts | 17 + .../presenter/InvoiceItem.presenter.ts.bak | 16 + .../InvoiceParticipant.presenter.ts.bak | 26 + ...InvoiceParticipantAddress.presenter.ts.bak | 19 + .../presenter/get-invoice.presenter.ts | 59 + .../get-invoice/presenter/index.ts | 1 + .../server/presentation/controllers/index.ts | 5 + .../controllers/list-invoices/index.ts | 17 + .../list-invoices/list-invoices.controller.ts | 47 + .../InvoiceParticipant.presenter.ts.bak | 22 + ...InvoiceParticipantAddress.presenter.ts.bak | 14 + .../list-invoices/presenter/index.ts | 1 + .../presenter/list-invoices.presenter.ts | 31 + .../controllers/update-invoice/index.ts.bak | 57 + .../presenter/InvoiceItem.presenter.ts.bak | 19 + .../InvoiceParticipant.presenter.ts.bak | 26 + ...InvoiceParticipantAddress.presenter.ts.bak | 19 + .../presenter/UpdateInvoice.presenter.ts.bak | 33 + .../update-invoice/presenter/index.ts.bak | 1 + .../update-invoice.controller.ts.bak | 72 + .../src/server/presentation/dto/index.ts | 3 + .../presentation/dto/invoices.request.dto.ts | 37 + .../presentation/dto/invoices.response.dto.ts | 76 + .../presentation/dto/invoices.schemas.ts | 43 + .../invoices/src/server/presentation/index.ts | 2 + modules/invoices/tsconfig.json | 16 + modules/invoices/turbo.json | 9 + package.json | 22 + packages/eslint-config/README.md | 3 + packages/eslint-config/library.js | 34 + packages/eslint-config/package.json | 20 + packages/eslint-config/react-internal.js | 39 + packages/eslint-config/server.js | 49 + packages/eslint-config/vite.js | 19 + packages/jest-presets/node/jest-preset.ts | 17 + packages/jest-presets/package.json | 16 + packages/rdx-auth/package.json | 81 + packages/rdx-auth/src/application/index.ts | 5 + .../src/application/list-users/index.ts | 1 + .../list-users/list-users.use-case.ts | 16 + .../rdx-auth/src/application/login/index.ts | 1 + .../src/application/login/login.use-case.ts | 15 + .../rdx-auth/src/application/logout/index.ts | 1 + .../src/application/logout/logout.use-case.ts | 15 + .../src/application/refresh-token/index.ts | 1 + .../refresh-token/refresh-token.use-case.ts | 22 + .../src/application/register/index.ts | 1 + .../application/register/register.use-case.ts | 22 + .../domain/aggregates/authenticated-user.ts | 90 + .../rdx-auth/src/domain/aggregates/index.ts | 4 + .../rdx-auth/src/domain/aggregates/role.ts | 15 + .../rdx-auth/src/domain/aggregates/user.ts | 67 + .../rdx-auth/src/domain/entities/index.ts | 5 + .../src/domain/entities/jwt-payload.ts | 67 + .../src/domain/entities/login-data.ts | 58 + .../src/domain/entities/logout-data.ts | 48 + .../src/domain/entities/register-data.ts | 59 + .../src/domain/entities/tab-context.ts | 49 + packages/rdx-auth/src/domain/events/index.ts | 1 + .../domain/events/user-authenticated.event.ts | 13 + packages/rdx-auth/src/domain/index.ts | 6 + ...authenticated-user-repository.interface.ts | 14 + .../rdx-auth/src/domain/repositories/index.ts | 4 + .../tab-context-repository.interface.ts | 20 + .../user-permission-repository.interface.ts | 1 + .../repositories/user-repository.interface.ts | 9 + .../domain/services/auth-service.interface.ts | 42 + .../src/domain/services/auth.service.ts | 204 + .../rdx-auth/src/domain/services/index.ts | 8 + .../services/tab-context-service.interface.ts | 15 + .../domain/services/tab-context.service.ts | 87 + .../domain/services/user-service.interface.ts | 8 + .../src/domain/services/user.service.ts | 40 + .../__tests__/hash-password.spec.ts | 44 + .../domain/value-objects/auth-user-roles.ts | 35 + .../src/domain/value-objects/hash-password.ts | 48 + .../src/domain/value-objects/index.ts | 5 + .../domain/value-objects/plain-password.ts | 36 + .../src/domain/value-objects/token.ts | 36 + .../src/domain/value-objects/username.ts | 41 + packages/rdx-auth/src/index.ts | 4 + .../src/infraestructure/express/index.ts | 1 + .../src/infraestructure/express/types.ts | 10 + .../rdx-auth/src/infraestructure/index.ts | 5 + .../mappers/authenticated-user.mapper.ts | 64 + .../src/infraestructure/mappers/index.ts | 3 + .../mappers/tab-context.mapper.ts | 62 + .../infraestructure/mappers/user.mapper.ts | 51 + .../src/infraestructure/middleware/index.ts | 2 + .../middleware/passport-auth.middleware.ts | 69 + .../middleware/tab-context.middleware.ts | 3 + .../src/infraestructure/passport/index.ts | 15 + .../infraestructure/passport/jwt.helper.ts | 14 + .../passport/passport-auth-provider.ts | 115 + .../src/infraestructure/passport/passport.ts | 26 + .../sequelize/auth-user.model.ts | 74 + .../authenticated-user.repository.ts | 95 + .../src/infraestructure/sequelize/index.ts | 25 + .../sequelize/tab-context.model.ts | 58 + .../sequelize/tab-context.repository.ts | 130 + .../infraestructure/sequelize/user.model.ts | 55 + .../sequelize/user.repository.ts | 72 + .../src/presentation/controllers/index.ts | 5 + .../controllers/listUsers/index.ts | 17 + .../listUsers/list-users.controller.ts | 37 + .../listUsers/list-users.presenter.ts | 16 + .../presentation/controllers/login/index.ts | 17 + .../controllers/login/login.controller.ts | 48 + .../controllers/login/login.presenter.ts | 56 + .../presentation/controllers/logout/index.ts | 16 + .../controllers/logout/logout.controller.ts | 49 + .../controllers/refreshToken/index.ts | 18 + .../refreshToken/refresh-token.controller.ts | 45 + .../refreshToken/refresh-token.presenter.ts | 14 + .../controllers/register/index.ts | 18 + .../register/register.controller.ts | 52 + .../register/register.presenter.ts | 21 + .../src/presentation/dto/auth.request.dto.ts | 10 + .../src/presentation/dto/auth.response.dto.ts | 24 + .../presentation/dto/auth.validation.dto.ts | 16 + .../rdx-auth/src/presentation/dto/index.ts | 7 + .../src/presentation/dto/user.request.dto.ts | 1 + .../src/presentation/dto/user.response.dto.ts | 5 + .../presentation/dto/user.validation.dto.ts | 3 + packages/rdx-auth/src/presentation/index.ts | 2 + packages/rdx-auth/tsconfig.json | 15 + packages/rdx-auth/turbo.json | 8 + packages/rdx-core/package.json | 80 + packages/rdx-core/src/index.ts | 2 + .../src/infrastructure/database/index.ts | 2 + .../database/transaction-manager.interface.ts | 27 + .../database/transaction-manager.ts | 60 + packages/rdx-core/src/infrastructure/index.ts | 2 + .../src/infrastructure/sequelize/index.ts | 3 + .../sequelize/sequelize-mapper.ts | 111 + .../sequelize/sequelize-repository.ts | 156 + .../sequelize-transaction-manager.ts | 27 + .../src/presentation/dto/error.dto.ts | 20 + .../rdx-core/src/presentation/dto/index.ts | 2 + .../src/presentation/dto/types.dto.ts | 15 + .../src/presentation/express/api-error.ts | 37 + .../express/express-controller.ts | 166 + .../src/presentation/express/index.ts | 4 + .../middlewares/global-error-handler.ts | 39 + .../presentation/express/middlewares/index.ts | 1 + .../express/validate-request-dto.ts | 38 + packages/rdx-core/src/presentation/index.ts | 2 + packages/rdx-core/tsconfig.json | 15 + packages/rdx-core/turbo.json | 8 + packages/rdx-criteria/package.json | 33 + packages/rdx-criteria/src/criteria.ts | 0 packages/rdx-criteria/src/index.ts | 0 .../rdx-criteria/src/pagination.ts/index.ts | 5 + .../src/pagination.ts/pagination-defaults.ts | 7 + packages/rdx-criteria/tsconfig.json | 15 + packages/rdx-ddd-domain/package.json | 40 + .../aggregate-root-repository.interface.ts | 1 + packages/rdx-ddd-domain/src/aggregate-root.ts | 39 + packages/rdx-ddd-domain/src/domain-entity.ts | 33 + .../src/events/domain-event-handle.ts | 3 + .../src/events/domain-event.interface.ts | 7 + .../rdx-ddd-domain/src/events/domain-event.ts | 135 + packages/rdx-ddd-domain/src/events/index.ts | 2 + packages/rdx-ddd-domain/src/index.ts | 5 + .../__tests__/email-address.test.ts | 61 + .../__tests__/money-value.test.ts | 54 + .../src/value-objects/__tests__/name.spec.ts | 41 + .../__tests__/percentage.test.ts | 51 + .../__tests__/phone-number.test.ts | 70 + .../__tests__/postal-address.test.ts | 64 + .../value-objects/__tests__/quantity.spec.ts | 112 + .../src/value-objects/__tests__/slug.spec.ts | 41 + .../__tests__/tin-number.test.ts | 40 + .../value-objects/__tests__/unique-id.test.ts | 59 + .../value-objects/__tests__/utc-date.test.ts | 41 + .../__tests__/value-objects.test.ts | 47 + .../src/value-objects/email-address.ts | 55 + .../rdx-ddd-domain/src/value-objects/index.ts | 12 + .../src/value-objects/money-value.ts | 216 + .../rdx-ddd-domain/src/value-objects/name.ts | 62 + .../src/value-objects/percentage.ts | 87 + .../src/value-objects/phone-number.ts | 62 + .../src/value-objects/postal-address.ts | 109 + .../src/value-objects/quantity.ts | 142 + .../rdx-ddd-domain/src/value-objects/slug.ts | 49 + .../src/value-objects/tin-number.ts | 51 + .../src/value-objects/unique-id.ts | 46 + .../src/value-objects/utc-date.ts | 74 + .../src/value-objects/value-object.ts | 25 + packages/rdx-ddd-domain/tsconfig.json | 15 + packages/rdx-ddd-domain/turbo.json | 9 + packages/rdx-logger/package.json | 44 + packages/rdx-logger/src/index.ts | 102 + packages/rdx-logger/tsconfig.json | 15 + packages/rdx-logger/turbo.json | 8 + packages/rdx-modules/package.json | 39 + packages/rdx-modules/src/index.ts | 3 + .../src/module-client.interface.ts | 16 + .../src/module-server.interface.ts | 10 + packages/rdx-modules/src/types.ts | 17 + packages/rdx-modules/tsconfig.json | 15 + packages/rdx-modules/turbo.json | 8 + packages/rdx-utils/package.json | 29 + packages/rdx-utils/src/helpers/collection.ts | 124 + packages/rdx-utils/src/helpers/index.ts | 4 + packages/rdx-utils/src/helpers/maybe.ts | 46 + packages/rdx-utils/src/helpers/result.ts | 92 + packages/rdx-utils/src/helpers/utils.ts | 12 + packages/rdx-utils/src/index.ts | 1 + packages/rdx-utils/tsconfig.json | 15 + packages/rdx-utils/turbo.json | 8 + packages/typescript-config/README.md | 3 + packages/typescript-config/base.json | 19 + packages/typescript-config/library.json | 9 + packages/typescript-config/package.json | 12 + packages/typescript-config/react-library.json | 7 + packages/typescript-config/vite.json | 16 + packages/ui/.eslintrc.js | 9 + packages/ui/components/counter.tsx | 11 + packages/ui/components/header.tsx | 13 + packages/ui/components/index.ts | 2 + packages/ui/index.ts | 2 + packages/ui/package.json | 27 + packages/ui/tsconfig.json | 5 + pnpm-lock.yaml | 8484 +++++++++++++++++ pnpm-workspace.yaml | 5 + scripts/create-package.ts | 65 + .../templates/client/__PACKAGE_NAME__Page.tsx | 3 + scripts/templates/client/manifest.ts | 12 + scripts/templates/client/tsconfig.json | 4 + scripts/templates/package.json | 23 + scripts/templates/server/controller.ts | 3 + scripts/templates/server/index.ts | 13 + scripts/templates/server/tsconfig.json | 10 + turbo.json | 26 + 342 files changed, 21314 insertions(+) create mode 100644 .dockerignore create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .prettierrc create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 apps/server/.env create mode 100644 apps/server/.env.development create mode 100644 apps/server/Dockerfile create mode 100644 apps/server/eslint.config.js create mode 100644 apps/server/package.json create mode 100644 apps/server/src/__tests__/server.test.ts create mode 100644 apps/server/src/app.ts create mode 100644 apps/server/src/config/database.ts create mode 100644 apps/server/src/config/index.ts create mode 100644 apps/server/src/config/register-models.ts create mode 100644 apps/server/src/core/helpers/index.ts create mode 100644 apps/server/src/core/helpers/model-loader.ts create mode 100644 apps/server/src/core/helpers/module-loader.ts create mode 100644 apps/server/src/core/helpers/service-registry.ts create mode 100644 apps/server/src/index.ts create mode 100644 apps/server/src/modules.ts create mode 100644 apps/server/tsconfig.json create mode 100644 apps/web/.eslintrc.cjs create mode 100644 apps/web/index.html create mode 100644 apps/web/package.json create mode 100644 apps/web/public/typescript.svg create mode 100644 apps/web/public/vite.svg create mode 100644 apps/web/src/main.d.ts create mode 100644 apps/web/src/main.d.ts.map create mode 100644 apps/web/src/main.js create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/style.css create mode 100644 apps/web/src/vite-env.d.ts create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/vite.config.ts create mode 100644 docker-compose.yml create mode 100644 docs/API.md create mode 100644 docs/DsRegistroVeriFactu.xlsx create mode 100644 docs/README.md create mode 100644 docs/REQUISITOS CLIENTES.md create mode 100644 docs/REQUISITOS GENERALES.md create mode 100644 modules/invoices/package.json create mode 100644 modules/invoices/src/client/index.ts create mode 100644 modules/invoices/src/index.ts create mode 100644 modules/invoices/src/server/application/create-invoice.use-case.ts create mode 100644 modules/invoices/src/server/application/delete-invoice.use-case.ts create mode 100644 modules/invoices/src/server/application/get-invoice.use-case.ts create mode 100644 modules/invoices/src/server/application/index.ts create mode 100644 modules/invoices/src/server/application/list-invoices.use-case.ts create mode 100644 modules/invoices/src/server/application/services/index.ts create mode 100644 modules/invoices/src/server/application/services/participantAddressFinder.ts.bak create mode 100644 modules/invoices/src/server/application/services/participantFinder.ts.bak create mode 100644 modules/invoices/src/server/application/update-invoice.use-case.bak create mode 100644 modules/invoices/src/server/domain/aggregates/index.ts create mode 100644 modules/invoices/src/server/domain/aggregates/invoice.ts create mode 100644 modules/invoices/src/server/domain/entities/index.ts create mode 100644 modules/invoices/src/server/domain/entities/invoice-customer/index.ts create mode 100644 modules/invoices/src/server/domain/entities/invoice-customer/invoice-address.ts create mode 100644 modules/invoices/src/server/domain/entities/invoice-customer/invoice-customer.ts create mode 100644 modules/invoices/src/server/domain/entities/invoice-items/__tests__/invoice-item.test.ts create mode 100644 modules/invoices/src/server/domain/entities/invoice-items/index.ts create mode 100644 modules/invoices/src/server/domain/entities/invoice-items/invoice-item.ts create mode 100644 modules/invoices/src/server/domain/entities/invoice-items/invoice-items.ts create mode 100644 modules/invoices/src/server/domain/index.ts create mode 100644 modules/invoices/src/server/domain/repositories/index.ts create mode 100644 modules/invoices/src/server/domain/repositories/invoice-repository.interface.ts create mode 100644 modules/invoices/src/server/domain/services/index.ts create mode 100644 modules/invoices/src/server/domain/services/invoice-service.interface.ts create mode 100644 modules/invoices/src/server/domain/services/invoice.service.ts create mode 100644 modules/invoices/src/server/domain/value-objects/index.ts create mode 100644 modules/invoices/src/server/domain/value-objects/invoice-address-type.ts create mode 100644 modules/invoices/src/server/domain/value-objects/invoice-item-description.ts create mode 100644 modules/invoices/src/server/domain/value-objects/invoice-number.ts create mode 100644 modules/invoices/src/server/domain/value-objects/invoice-serie.ts create mode 100644 modules/invoices/src/server/domain/value-objects/invoice-status.ts create mode 100644 modules/invoices/src/server/index.ts create mode 100644 modules/invoices/src/server/intrastructure/express/index.ts create mode 100644 modules/invoices/src/server/intrastructure/express/invoices.routes.ts create mode 100644 modules/invoices/src/server/intrastructure/index.ts create mode 100644 modules/invoices/src/server/intrastructure/mappers/contact.mapper.ts.bak create mode 100644 modules/invoices/src/server/intrastructure/mappers/contactAddress.mapper.ts.bak create mode 100644 modules/invoices/src/server/intrastructure/mappers/index.ts create mode 100644 modules/invoices/src/server/intrastructure/mappers/invoice-item.mapper.ts create mode 100644 modules/invoices/src/server/intrastructure/mappers/invoice.mapper.ts create mode 100644 modules/invoices/src/server/intrastructure/mappers/invoiceParticipant.mapper.ts.bak create mode 100644 modules/invoices/src/server/intrastructure/mappers/invoiceParticipantAddress.mapper.ts.bak create mode 100644 modules/invoices/src/server/intrastructure/sequelize/contact.mo.del.ts.bak create mode 100644 modules/invoices/src/server/intrastructure/sequelize/contactAddress.mo.del.ts.bak create mode 100644 modules/invoices/src/server/intrastructure/sequelize/index.ts create mode 100644 modules/invoices/src/server/intrastructure/sequelize/invoice-item.model.ts create mode 100644 modules/invoices/src/server/intrastructure/sequelize/invoice.model.ts create mode 100644 modules/invoices/src/server/intrastructure/sequelize/invoice.repository.ts create mode 100644 modules/invoices/src/server/intrastructure/sequelize/invoiceParticipant.mo.del.ts.bak create mode 100644 modules/invoices/src/server/intrastructure/sequelize/invoiceParticipantAddress.mo.del.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/create-invoice/create-invoice.controller.ts create mode 100644 modules/invoices/src/server/presentation/controllers/create-invoice/index.ts create mode 100644 modules/invoices/src/server/presentation/controllers/create-invoice/presenter/InvoiceItem.presenter.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/create-invoice/presenter/InvoiceParticipant.presenter.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/create-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/create-invoice/presenter/create-invoice.presenter.ts create mode 100644 modules/invoices/src/server/presentation/controllers/create-invoice/presenter/index.ts create mode 100644 modules/invoices/src/server/presentation/controllers/delete-invoice/delete-invoice.controller.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/delete-invoice/index.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/get-invoice/get-invoice.controller.ts create mode 100644 modules/invoices/src/server/presentation/controllers/get-invoice/index.ts create mode 100644 modules/invoices/src/server/presentation/controllers/get-invoice/presenter/InvoiceItem.presenter.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/get-invoice/presenter/InvoiceParticipant.presenter.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/get-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/get-invoice/presenter/get-invoice.presenter.ts create mode 100644 modules/invoices/src/server/presentation/controllers/get-invoice/presenter/index.ts create mode 100644 modules/invoices/src/server/presentation/controllers/index.ts create mode 100644 modules/invoices/src/server/presentation/controllers/list-invoices/index.ts create mode 100644 modules/invoices/src/server/presentation/controllers/list-invoices/list-invoices.controller.ts create mode 100644 modules/invoices/src/server/presentation/controllers/list-invoices/presenter/InvoiceParticipant.presenter.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/list-invoices/presenter/InvoiceParticipantAddress.presenter.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/list-invoices/presenter/index.ts create mode 100644 modules/invoices/src/server/presentation/controllers/list-invoices/presenter/list-invoices.presenter.ts create mode 100644 modules/invoices/src/server/presentation/controllers/update-invoice/index.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/update-invoice/presenter/InvoiceItem.presenter.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/update-invoice/presenter/InvoiceParticipant.presenter.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/update-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/update-invoice/presenter/UpdateInvoice.presenter.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/update-invoice/presenter/index.ts.bak create mode 100644 modules/invoices/src/server/presentation/controllers/update-invoice/update-invoice.controller.ts.bak create mode 100644 modules/invoices/src/server/presentation/dto/index.ts create mode 100644 modules/invoices/src/server/presentation/dto/invoices.request.dto.ts create mode 100644 modules/invoices/src/server/presentation/dto/invoices.response.dto.ts create mode 100644 modules/invoices/src/server/presentation/dto/invoices.schemas.ts create mode 100644 modules/invoices/src/server/presentation/index.ts create mode 100644 modules/invoices/tsconfig.json create mode 100644 modules/invoices/turbo.json create mode 100644 package.json create mode 100644 packages/eslint-config/README.md create mode 100644 packages/eslint-config/library.js create mode 100644 packages/eslint-config/package.json create mode 100644 packages/eslint-config/react-internal.js create mode 100644 packages/eslint-config/server.js create mode 100644 packages/eslint-config/vite.js create mode 100644 packages/jest-presets/node/jest-preset.ts create mode 100644 packages/jest-presets/package.json create mode 100644 packages/rdx-auth/package.json create mode 100644 packages/rdx-auth/src/application/index.ts create mode 100644 packages/rdx-auth/src/application/list-users/index.ts create mode 100644 packages/rdx-auth/src/application/list-users/list-users.use-case.ts create mode 100644 packages/rdx-auth/src/application/login/index.ts create mode 100644 packages/rdx-auth/src/application/login/login.use-case.ts create mode 100644 packages/rdx-auth/src/application/logout/index.ts create mode 100644 packages/rdx-auth/src/application/logout/logout.use-case.ts create mode 100644 packages/rdx-auth/src/application/refresh-token/index.ts create mode 100644 packages/rdx-auth/src/application/refresh-token/refresh-token.use-case.ts create mode 100644 packages/rdx-auth/src/application/register/index.ts create mode 100644 packages/rdx-auth/src/application/register/register.use-case.ts create mode 100644 packages/rdx-auth/src/domain/aggregates/authenticated-user.ts create mode 100644 packages/rdx-auth/src/domain/aggregates/index.ts create mode 100644 packages/rdx-auth/src/domain/aggregates/role.ts create mode 100644 packages/rdx-auth/src/domain/aggregates/user.ts create mode 100644 packages/rdx-auth/src/domain/entities/index.ts create mode 100644 packages/rdx-auth/src/domain/entities/jwt-payload.ts create mode 100644 packages/rdx-auth/src/domain/entities/login-data.ts create mode 100644 packages/rdx-auth/src/domain/entities/logout-data.ts create mode 100644 packages/rdx-auth/src/domain/entities/register-data.ts create mode 100644 packages/rdx-auth/src/domain/entities/tab-context.ts create mode 100644 packages/rdx-auth/src/domain/events/index.ts create mode 100644 packages/rdx-auth/src/domain/events/user-authenticated.event.ts create mode 100644 packages/rdx-auth/src/domain/index.ts create mode 100644 packages/rdx-auth/src/domain/repositories/authenticated-user-repository.interface.ts create mode 100644 packages/rdx-auth/src/domain/repositories/index.ts create mode 100644 packages/rdx-auth/src/domain/repositories/tab-context-repository.interface.ts create mode 100644 packages/rdx-auth/src/domain/repositories/user-permission-repository.interface.ts create mode 100644 packages/rdx-auth/src/domain/repositories/user-repository.interface.ts create mode 100644 packages/rdx-auth/src/domain/services/auth-service.interface.ts create mode 100644 packages/rdx-auth/src/domain/services/auth.service.ts create mode 100644 packages/rdx-auth/src/domain/services/index.ts create mode 100644 packages/rdx-auth/src/domain/services/tab-context-service.interface.ts create mode 100644 packages/rdx-auth/src/domain/services/tab-context.service.ts create mode 100644 packages/rdx-auth/src/domain/services/user-service.interface.ts create mode 100644 packages/rdx-auth/src/domain/services/user.service.ts create mode 100644 packages/rdx-auth/src/domain/value-objects/__tests__/hash-password.spec.ts create mode 100644 packages/rdx-auth/src/domain/value-objects/auth-user-roles.ts create mode 100644 packages/rdx-auth/src/domain/value-objects/hash-password.ts create mode 100644 packages/rdx-auth/src/domain/value-objects/index.ts create mode 100644 packages/rdx-auth/src/domain/value-objects/plain-password.ts create mode 100644 packages/rdx-auth/src/domain/value-objects/token.ts create mode 100644 packages/rdx-auth/src/domain/value-objects/username.ts create mode 100644 packages/rdx-auth/src/index.ts create mode 100644 packages/rdx-auth/src/infraestructure/express/index.ts create mode 100644 packages/rdx-auth/src/infraestructure/express/types.ts create mode 100644 packages/rdx-auth/src/infraestructure/index.ts create mode 100644 packages/rdx-auth/src/infraestructure/mappers/authenticated-user.mapper.ts create mode 100644 packages/rdx-auth/src/infraestructure/mappers/index.ts create mode 100644 packages/rdx-auth/src/infraestructure/mappers/tab-context.mapper.ts create mode 100644 packages/rdx-auth/src/infraestructure/mappers/user.mapper.ts create mode 100644 packages/rdx-auth/src/infraestructure/middleware/index.ts create mode 100644 packages/rdx-auth/src/infraestructure/middleware/passport-auth.middleware.ts create mode 100644 packages/rdx-auth/src/infraestructure/middleware/tab-context.middleware.ts create mode 100644 packages/rdx-auth/src/infraestructure/passport/index.ts create mode 100644 packages/rdx-auth/src/infraestructure/passport/jwt.helper.ts create mode 100644 packages/rdx-auth/src/infraestructure/passport/passport-auth-provider.ts create mode 100644 packages/rdx-auth/src/infraestructure/passport/passport.ts create mode 100644 packages/rdx-auth/src/infraestructure/sequelize/auth-user.model.ts create mode 100644 packages/rdx-auth/src/infraestructure/sequelize/authenticated-user.repository.ts create mode 100644 packages/rdx-auth/src/infraestructure/sequelize/index.ts create mode 100644 packages/rdx-auth/src/infraestructure/sequelize/tab-context.model.ts create mode 100644 packages/rdx-auth/src/infraestructure/sequelize/tab-context.repository.ts create mode 100644 packages/rdx-auth/src/infraestructure/sequelize/user.model.ts create mode 100644 packages/rdx-auth/src/infraestructure/sequelize/user.repository.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/index.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/listUsers/index.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/listUsers/list-users.controller.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/listUsers/list-users.presenter.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/login/index.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/login/login.controller.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/login/login.presenter.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/logout/index.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/logout/logout.controller.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/refreshToken/index.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/refreshToken/refresh-token.controller.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/refreshToken/refresh-token.presenter.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/register/index.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/register/register.controller.ts create mode 100644 packages/rdx-auth/src/presentation/controllers/register/register.presenter.ts create mode 100644 packages/rdx-auth/src/presentation/dto/auth.request.dto.ts create mode 100644 packages/rdx-auth/src/presentation/dto/auth.response.dto.ts create mode 100644 packages/rdx-auth/src/presentation/dto/auth.validation.dto.ts create mode 100644 packages/rdx-auth/src/presentation/dto/index.ts create mode 100644 packages/rdx-auth/src/presentation/dto/user.request.dto.ts create mode 100644 packages/rdx-auth/src/presentation/dto/user.response.dto.ts create mode 100644 packages/rdx-auth/src/presentation/dto/user.validation.dto.ts create mode 100644 packages/rdx-auth/src/presentation/index.ts create mode 100644 packages/rdx-auth/tsconfig.json create mode 100644 packages/rdx-auth/turbo.json create mode 100644 packages/rdx-core/package.json create mode 100644 packages/rdx-core/src/index.ts create mode 100644 packages/rdx-core/src/infrastructure/database/index.ts create mode 100644 packages/rdx-core/src/infrastructure/database/transaction-manager.interface.ts create mode 100644 packages/rdx-core/src/infrastructure/database/transaction-manager.ts create mode 100644 packages/rdx-core/src/infrastructure/index.ts create mode 100644 packages/rdx-core/src/infrastructure/sequelize/index.ts create mode 100644 packages/rdx-core/src/infrastructure/sequelize/sequelize-mapper.ts create mode 100644 packages/rdx-core/src/infrastructure/sequelize/sequelize-repository.ts create mode 100644 packages/rdx-core/src/infrastructure/sequelize/sequelize-transaction-manager.ts create mode 100644 packages/rdx-core/src/presentation/dto/error.dto.ts create mode 100644 packages/rdx-core/src/presentation/dto/index.ts create mode 100644 packages/rdx-core/src/presentation/dto/types.dto.ts create mode 100644 packages/rdx-core/src/presentation/express/api-error.ts create mode 100644 packages/rdx-core/src/presentation/express/express-controller.ts create mode 100644 packages/rdx-core/src/presentation/express/index.ts create mode 100644 packages/rdx-core/src/presentation/express/middlewares/global-error-handler.ts create mode 100644 packages/rdx-core/src/presentation/express/middlewares/index.ts create mode 100644 packages/rdx-core/src/presentation/express/validate-request-dto.ts create mode 100644 packages/rdx-core/src/presentation/index.ts create mode 100644 packages/rdx-core/tsconfig.json create mode 100644 packages/rdx-core/turbo.json create mode 100644 packages/rdx-criteria/package.json create mode 100644 packages/rdx-criteria/src/criteria.ts create mode 100644 packages/rdx-criteria/src/index.ts create mode 100644 packages/rdx-criteria/src/pagination.ts/index.ts create mode 100644 packages/rdx-criteria/src/pagination.ts/pagination-defaults.ts create mode 100644 packages/rdx-criteria/tsconfig.json create mode 100644 packages/rdx-ddd-domain/package.json create mode 100644 packages/rdx-ddd-domain/src/aggregate-root-repository.interface.ts create mode 100644 packages/rdx-ddd-domain/src/aggregate-root.ts create mode 100644 packages/rdx-ddd-domain/src/domain-entity.ts create mode 100644 packages/rdx-ddd-domain/src/events/domain-event-handle.ts create mode 100644 packages/rdx-ddd-domain/src/events/domain-event.interface.ts create mode 100644 packages/rdx-ddd-domain/src/events/domain-event.ts create mode 100644 packages/rdx-ddd-domain/src/events/index.ts create mode 100644 packages/rdx-ddd-domain/src/index.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/__tests__/email-address.test.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/__tests__/money-value.test.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/__tests__/name.spec.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/__tests__/percentage.test.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/__tests__/phone-number.test.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/__tests__/postal-address.test.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/__tests__/quantity.spec.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/__tests__/slug.spec.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/__tests__/tin-number.test.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/__tests__/unique-id.test.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/__tests__/utc-date.test.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/__tests__/value-objects.test.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/email-address.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/index.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/money-value.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/name.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/percentage.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/phone-number.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/postal-address.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/quantity.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/slug.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/tin-number.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/unique-id.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/utc-date.ts create mode 100644 packages/rdx-ddd-domain/src/value-objects/value-object.ts create mode 100644 packages/rdx-ddd-domain/tsconfig.json create mode 100644 packages/rdx-ddd-domain/turbo.json create mode 100644 packages/rdx-logger/package.json create mode 100644 packages/rdx-logger/src/index.ts create mode 100644 packages/rdx-logger/tsconfig.json create mode 100644 packages/rdx-logger/turbo.json create mode 100644 packages/rdx-modules/package.json create mode 100644 packages/rdx-modules/src/index.ts create mode 100644 packages/rdx-modules/src/module-client.interface.ts create mode 100644 packages/rdx-modules/src/module-server.interface.ts create mode 100644 packages/rdx-modules/src/types.ts create mode 100644 packages/rdx-modules/tsconfig.json create mode 100644 packages/rdx-modules/turbo.json create mode 100644 packages/rdx-utils/package.json create mode 100644 packages/rdx-utils/src/helpers/collection.ts create mode 100644 packages/rdx-utils/src/helpers/index.ts create mode 100644 packages/rdx-utils/src/helpers/maybe.ts create mode 100644 packages/rdx-utils/src/helpers/result.ts create mode 100644 packages/rdx-utils/src/helpers/utils.ts create mode 100644 packages/rdx-utils/src/index.ts create mode 100644 packages/rdx-utils/tsconfig.json create mode 100644 packages/rdx-utils/turbo.json create mode 100644 packages/typescript-config/README.md create mode 100644 packages/typescript-config/base.json create mode 100644 packages/typescript-config/library.json create mode 100644 packages/typescript-config/package.json create mode 100644 packages/typescript-config/react-library.json create mode 100644 packages/typescript-config/vite.json create mode 100644 packages/ui/.eslintrc.js create mode 100644 packages/ui/components/counter.tsx create mode 100644 packages/ui/components/header.tsx create mode 100644 packages/ui/components/index.ts create mode 100644 packages/ui/index.ts create mode 100644 packages/ui/package.json create mode 100644 packages/ui/tsconfig.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 scripts/create-package.ts create mode 100644 scripts/templates/client/__PACKAGE_NAME__Page.tsx create mode 100644 scripts/templates/client/manifest.ts create mode 100644 scripts/templates/client/tsconfig.json create mode 100644 scripts/templates/package.json create mode 100644 scripts/templates/server/controller.ts create mode 100644 scripts/templates/server/index.ts create mode 100644 scripts/templates/server/tsconfig.json create mode 100644 turbo.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..02f791d --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,5 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["@repo/eslint-config/index.js"], +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34836da --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js + +# testing +coverage + +# next.js +.next/ +out/ +build + +# other +dist/ + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# turbo +.turbo diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..ded82e2 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +auto-install-peers = true diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..91a4315 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "bracketSpacing": true, + "useTabs": false, + "printWidth": 100, + "tabWidth": 2, + "semi": true, + "singleQuote": false, + "trailingComma": "es5", + "jsxSingleQuote": true, + "jsxBracketSameLine": false, + "arrowParens": "always", + "rcVerbose": true +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d5e60c9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,60 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch firefox localhost", + "type": "firefox", + "request": "launch", + "reAttach": true, + "url": "http://localhost:5173", + "webRoot": "${workspaceFolder}/client" + }, + + { + "name": "Launch Chrome localhost", + "type": "chrome", + "request": "launch", + "url": "http://localhost:5173", + "webRoot": "${workspaceFolder}/client" + }, + + { + "type": "msedge", + "request": "launch", + "name": "CLIENT: Launch Edge against localhost", + "url": "http://localhost:5173", + "webRoot": "${workspaceFolder}/client" + }, + + { + "type": "node", + "request": "attach", + "name": "Attach to ts-node-dev", + "port": 4321, + "restart": true, + "timeout": 10000, + "sourceMaps": true, + "resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"] + }, + + { + "name": "Launch via YARN", + "request": "launch", + "runtimeArgs": ["run", "server"], + "runtimeExecutable": "yarn", + "skipFiles": ["/**", "client/**", "dist/**", "doc/**"], + "type": "node" + }, + + { + "name": "Turbo: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "pnpm run dev --filter=server", + "skipFiles": ["/**"], + "env": { + "NODE_OPTIONS": "--inspect" + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..465c6f2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,30 @@ +{ + "eslint.workingDirectories": [ + { + "mode": "auto" + } + ], + + "typescript.preferences.importModuleSpecifier": "shortest", + "javascript.preferences.importModuleSpecifier": "shortest", + "javascript.suggest.autoImports": true, + "typescript.suggest.autoImports": true, + "typescript.suggest.completeFunctionCalls": true, + "typescript.suggest.includeAutomaticOptionalChainCompletions": true, + + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + "source.fixAll.eslint": "explicit" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.formatOnPaste": false, + "prettier.useEditorConfig": false, + "prettier.useTabs": false, + "prettier.configPath": ".prettierrc", + + // other vscode settings + "[handlebars]": { + "editor.defaultFormatter": "vscode.html-language-features" + } // <- your root font size here +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..df202f6 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Turborepo Docker starter + +This is a community-maintained example. If you experience a problem, please submit a pull request with a fix. GitHub Issues will be closed. + +## Using this example + +Run the following command: + +```sh +npx create-turbo@latest -e with-docker +``` + +## What's inside? + +This Turborepo includes the following: + +### Apps and Packages + +- `web`: a [Next.js](https://nextjs.org/) app +- `api`: an [Express](https://expressjs.com/) server +- `@repo/ui`: a React component library +- `@repo/logger`: Isomorphic logger (a small wrapper around console.log) +- `@repo/eslint-config`: ESLint presets +- `@repo/typescript-config`: tsconfig.json's used throughout the monorepo +- `@repo/jest-presets`: Jest configurations + +Each package/app is 100% [TypeScript](https://www.typescriptlang.org/). + +### Docker + +This repo is configured to be built with Docker, and Docker compose. To build all apps in this repo: + +``` +# Install dependencies +yarn install + +# Create a network, which allows containers to communicate +# with each other, by using their container name as a hostname +docker network create app_network + +# Build prod using new BuildKit engine +COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f docker-compose.yml build + +# Start prod in detached mode +docker-compose -f docker-compose.yml up -d +``` + +Open http://localhost:3000. + +To shutdown all running containers: + +``` +# Stop running containers started by docker-compse + docker-compose -f docker-compose.yml down +``` + +### Remote Caching + +> [!TIP] +> Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache). + +This example includes optional remote caching. In the Dockerfiles of the apps, uncomment the build arguments for `TURBO_TEAM` and `TURBO_TOKEN`. Then, pass these build arguments to your Docker build. + +You can test this behavior using a command like: + +`docker build -f apps/web/Dockerfile . --build-arg TURBO_TEAM=“your-team-name” --build-arg TURBO_TOKEN=“your-token“ --no-cache` + +### Utilities + +This Turborepo has some additional tools already setup for you: + +- [TypeScript](https://www.typescriptlang.org/) for static type checking +- [ESLint](https://eslint.org/) for code linting +- [Jest](https://jestjs.io) test runner for all things JavaScript +- [Prettier](https://prettier.io) for code formatting diff --git a/apps/server/.env b/apps/server/.env new file mode 100644 index 0000000..9e06429 --- /dev/null +++ b/apps/server/.env @@ -0,0 +1,11 @@ +DB_HOST=localhost +DB_USER=rodax +DB_PASSWORD=rodax +DB_NAME=uecko_erp +DB_PORT=3306 + +PORT=3002 + +JWT_SECRET=supersecretkey +JWT_ACCESS_EXPIRATION=1h +JWT_REFRESH_EXPIRATION=7d diff --git a/apps/server/.env.development b/apps/server/.env.development new file mode 100644 index 0000000..99ff320 --- /dev/null +++ b/apps/server/.env.development @@ -0,0 +1,11 @@ +DB_HOST=localhost +DB_USER=rodax +DB_PASSWORD=rodax +DB_NAME=uecko_erp +DB_PORT=3306 + +PORT=3002 + +JWT_SECRET=supersecretkey +JWT_ACCESS_EXPIRATION=1h +JWT_REFRESH_EXPIRATION=7d diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile new file mode 100644 index 0000000..ddeb0d6 --- /dev/null +++ b/apps/server/Dockerfile @@ -0,0 +1,47 @@ +FROM node:18-alpine AS base + +# The web Dockerfile is copy-pasted into our main docs at /docs/handbook/deploying-with-docker. +# Make sure you update this Dockerfile, the Dockerfile in the web workspace and copy that over to Dockerfile in the docs. + +FROM base AS builder +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk update +RUN apk add --no-cache libc6-compat +# Set working directory +WORKDIR /app +RUN yarn global add turbo +COPY . . +RUN turbo prune api --docker + +# Add lockfile and package.json's of isolated subworkspace +FROM base AS installer +RUN apk update +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# First install dependencies (as they change less often) +COPY --from=builder /app/out/json/ . +RUN yarn install + +# Build the project and its dependencies +COPY --from=builder /app/out/full/ . + +# Uncomment and use build args to enable remote caching +# ARG TURBO_TEAM +# ENV TURBO_TEAM=$TURBO_TEAM + +# ARG TURBO_TOKEN +# ENV TURBO_TOKEN=$TURBO_TOKEN + +RUN yarn turbo build + +FROM base AS runner +WORKDIR /app + +# Don't run production as root +RUN addgroup --system --gid 1001 expressjs +RUN adduser --system --uid 1001 expressjs +USER expressjs +COPY --from=installer /app . + +CMD node apps/api/dist/index.js diff --git a/apps/server/eslint.config.js b/apps/server/eslint.config.js new file mode 100644 index 0000000..006c3f4 --- /dev/null +++ b/apps/server/eslint.config.js @@ -0,0 +1,10 @@ +import config from "@repo/eslint-config/index.js"; + +/** @type {import("eslint").Linter.FlatConfig} */ +export default [ + { + ...config, + files: ["**/*.js", "**/*.ts", "**/*.tsx"], + ignores: ["node_modules", "dist", "build"], + }, +]; diff --git a/apps/server/package.json b/apps/server/package.json new file mode 100644 index 0000000..729328f --- /dev/null +++ b/apps/server/package.json @@ -0,0 +1,77 @@ +{ + "name": "server", + "version": "0.0.0", + "private": true, + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "clean": "rm -rf dist && rm -rf node_modules", + "dev": "nodemon --exec \"node -r esbuild-register ./src/index.ts\" -e .ts", + "lint": "tsc --noEmit && eslint \"src/**/*.ts*\" --max-warnings 0", + "start": "node -r esbuild-register ./src/index.ts", + "test": "jest --detectOpenHandles" + }, + "jest": { + "preset": "@repo/jest-presets/node" + }, + "dependencies": { + "@rdx/core": "workspace:*", + "@rdx/ddd-domain": "workspace:*", + "@rdx/logger": "workspace:*", + "@rdx/modules": "workspace:*", + "@rdx/utils": "workspace:*", + "@modules/invoices": "workspace:*", + "bcrypt": "^5.1.1", + "body-parser": "^2.2.0", + "cors": "^2.8.5", + "dinero.js": "^1.9.1", + "dotenv": "^16.5.0", + "express": "^4.21.2", + "helmet": "^8.1.0", + "http-status": "^2.1.0", + "jsonwebtoken": "^9.0.2", + "libphonenumber-js": "^1.11.20", + "luxon": "^3.5.0", + "module-alias": "^2.2.3", + "mysql2": "^3.12.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", + "path": "^0.12.7", + "reflect-metadata": "^0.2.2", + "response-time": "^2.3.3", + "sequelize": "^6.37.7", + "zod": "^3.24.3" + }, + "devDependencies": { + "@jest/globals": "^29.7.0", + "@repo/eslint-config": "workspace:*", + "@repo/jest-presets": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/bcrypt": "^5.0.2", + "@types/body-parser": "^1.19.5", + "@types/cors": "^2.8.17", + "@types/dinero.js": "^1.9.4", + "@types/express": "^4.17.21", + "@types/glob": "^8.1.0", + "@types/jest": "^29.5.14", + "@types/jsonwebtoken": "^9.0.9", + "@types/luxon": "^3.6.2", + "@types/morgan": "^1.9.9", + "@types/node": "^22.15.2", + "@types/passport": "^1.0.17", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", + "@types/response-time": "^2.3.8", + "@types/supertest": "^6.0.3", + "@typescript-eslint/eslint-plugin": "^8.31.0", + "@typescript-eslint/parser": "^8.31.0", + "esbuild": "^0.25.3", + "esbuild-register": "^3.6.0", + "eslint": "^9.25.1", + "jest": "^29.7.0", + "nodemon": "^3.1.10", + "supertest": "^7.1.0", + "typescript": "5.8.3" + } +} diff --git a/apps/server/src/__tests__/server.test.ts b/apps/server/src/__tests__/server.test.ts new file mode 100644 index 0000000..7e9f0e3 --- /dev/null +++ b/apps/server/src/__tests__/server.test.ts @@ -0,0 +1,23 @@ +import supertest from "supertest"; +import { describe, it, expect } from "@jest/globals"; +import { createApp } from "../app"; + +describe("app", () => { + it("status check returns 200", async () => { + await supertest(createApp()) + .get("/status") + .expect(200) + .then((res) => { + expect(res.body.ok).toBe(true); + }); + }); + + it("message endpoint says hello", async () => { + await supertest(createApp()) + .get("/message/jared") + .expect(200) + .then((res) => { + expect(res.body.message).toBe("hello jared"); + }); + }); +}); diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts new file mode 100644 index 0000000..fa2a3a8 --- /dev/null +++ b/apps/server/src/app.ts @@ -0,0 +1,56 @@ +import { globalErrorHandler } from "@rdx/core"; +import { logger } from "@rdx/logger"; +import dotenv from "dotenv"; +import express, { Application } from "express"; +import helmet from "helmet"; +import responseTime from "response-time"; + +dotenv.config(); + +export function createApp(): Application { + const app = express(); + app.set("port", process.env.PORT ?? 3002); + + // secure apps by setting various HTTP headers + app.disable("x-powered-by"); + + // Middlewares + app.use(express.json()); + app.use(express.text()); + app.use(express.urlencoded({ extended: true })); + + app.use(responseTime()); // set up the response-time middleware + + // secure apps by setting various HTTP headers + app.use(helmet()); + + // Middleware global para desactivar la caché en todas las rutas + app.use((req, res, next) => { + res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"); + res.setHeader("Pragma", "no-cache"); + res.setHeader("Expires", "0"); + res.setHeader("etag", "false"); + next(); // Continúa con la siguiente función middleware o la ruta + }); + + // Inicializar Auth Provider + app.use((req, res, next) => { + //authProvider.initialize(); + next(); + }); + + app.use((req, _, next) => { + logger.info(`▶️ Incoming request ${req.method} to ${req.path}`); + next(); + }); + + // Registrar rutas de la API + // app.use("/api/v1", v1Routes()); + + // Gestión global de errores. + // Siempre al final de la cadena de middlewares + // y después de las rutas. + app.use(globalErrorHandler); + + return app; +} diff --git a/apps/server/src/config/database.ts b/apps/server/src/config/database.ts new file mode 100644 index 0000000..0f99d3d --- /dev/null +++ b/apps/server/src/config/database.ts @@ -0,0 +1,48 @@ +import { logger } from "@rdx/logger"; +import dotenv from "dotenv"; +import { Sequelize } from "sequelize"; + +dotenv.config(); + +export const sequelize = new Sequelize( + process.env.DB_NAME as string, // database + process.env.DB_USER as string, // username + process.env.DB_PASSWORD as string, // password + { + host: process.env.DB_HOST as string, + dialect: "mysql", + port: parseInt(process.env.DB_PORT || "3306", 10), + dialectOptions: { + multipleStatements: true, + dateStrings: true, + typeCast: true, + //timezone: "Z", + }, + pool: { + max: 10, + min: 0, + acquire: 30000, + idle: 10000, + }, + logQueryParameters: true, + logging: process.env.DB_LOGGING === "true" ? logger.debug : false, + define: { + charset: "utf8mb4", + collate: "utf8mb4_unicode_ci", + //freezeTableName: true, + underscored: true, + timestamps: true, + }, + } +); + +export async function connectToDatabase(): Promise { + try { + await sequelize.authenticate(); + //await registerModels(); + logger.info(`✔️${" "}Database connection established successfully.`); + } catch (error) { + logger.error("❌ Unable to connect to the database:", error); + process.exit(1); + } +} diff --git a/apps/server/src/config/index.ts b/apps/server/src/config/index.ts new file mode 100644 index 0000000..a3a5931 --- /dev/null +++ b/apps/server/src/config/index.ts @@ -0,0 +1,12 @@ +import dotenv from "dotenv"; +export * from "./database"; + +// Carga variables de entorno desde el archivo .env +dotenv.config(); + +// Exporta una configuración centralizada, aplicando valores por defecto donde sea necesario +export const ENV = { + HOST: process.env.HOST || process.env.HOSTNAME || "localhost", + PORT: process.env.PORT || "18888", + NODE_ENV: process.env.NODE_ENV || "development", +}; diff --git a/apps/server/src/config/register-models.ts b/apps/server/src/config/register-models.ts new file mode 100644 index 0000000..471fda8 --- /dev/null +++ b/apps/server/src/config/register-models.ts @@ -0,0 +1,66 @@ +import * as glob from "glob"; +import * as path from "path"; +import { DataTypes } from "sequelize"; +import { sequelize } from "./database"; +import { logger } from "@rdx/logger"; + +/** + * 🔹 Registra todos los modelos en Sequelize + */ +export const registerModels = async () => { + const cwd = path.resolve(`${__dirname}/../`); + const models: { [key: string]: any } = {}; + + // Opciones para buscar los modelos + const globOptions = { + cwd, + nocase: true, + nodir: true, + absolute: false, + }; + + try { + logger.info(`🔎 Searching models in: ${cwd}`); + + // Buscamos los ficheros que terminen en .model.js o .model.ts + glob.sync("**/*.model.{js,ts}", globOptions).forEach((file) => { + //logger.info(`📄 File >> ${file}...`); + const modelDef = require(path.join(file)).default; + const model = typeof modelDef === "function" ? modelDef(sequelize, DataTypes) : false; + + if (model) { + models[model.name] = model; + logger.info(`🔸 Model >> ${model.name} (${file})`); + } else { + logger.info(`🚫 No model`); + } + }); + + // Asociaciones y hooks de los modelos, si existen + for (const modelName in models) { + const model = models[modelName]; + if (model.associate) { + model.associate(sequelize, models); + } + if (model.hooks) { + model.hooks(sequelize); + } + } + } catch (error) { + logger.error("❌ Error registering models:", error); + process.exit(1); + } + + try { + // Sincronizamos DB en modo desarrollo + if (process.env.NODE_ENV !== "production") { + await sequelize.sync({ force: false, alter: true }); + logger.info(`✔️${" "}Database synchronized successfully.`); + } else { + logger.warning("⚠️ Running in production mode - Skipping database sync."); + } + } catch (error) { + logger.error("❌ Error synchronizing database:", error); + process.exit(1); + } +}; diff --git a/apps/server/src/core/helpers/index.ts b/apps/server/src/core/helpers/index.ts new file mode 100644 index 0000000..5cdba46 --- /dev/null +++ b/apps/server/src/core/helpers/index.ts @@ -0,0 +1,2 @@ +export * from "./module-loader"; +export * from "./service-registry"; diff --git a/apps/server/src/core/helpers/model-loader.ts b/apps/server/src/core/helpers/model-loader.ts new file mode 100644 index 0000000..d3ce598 --- /dev/null +++ b/apps/server/src/core/helpers/model-loader.ts @@ -0,0 +1,54 @@ +import { logger } from "@rdx/logger"; +import { ModelInitializer } from "@rdx/modules"; +import { Sequelize } from "sequelize"; + +const registeredModels: Map = new Map(); +const initializedModels = new Set(); + +/** + * 🔹 Registra todos los modelos en Sequelize + */ +export const registerModel = (models: ModelInitializer[], database: Sequelize) => { + for (const initModelFn of models) { + const model = initModelFn(database); + if (model) { + registeredModels.set(model.name, model); + } + } +}; + +export const initModels = async (sequelize: Sequelize) => { + registeredModels.forEach((_, name) => loadModel(name, sequelize)); + + try { + // Sincronizamos DB en modo desarrollo + if (process.env.NODE_ENV !== "production") { + await sequelize.sync({ force: false, alter: true }); + logger.info(`✔️${" "}Database synchronized successfully.`); + } else { + logger.warning("⚠️ Running in production mode - Skipping database sync."); + } + } catch (error) { + logger.error("❌ Error synchronizing database:", error); + process.exit(1); + } +}; + +export const loadModel = (name: string, sequelize: Sequelize) => { + if (initializedModels.has(name)) return; + + const model = registeredModels.get(name); + if (!model) throw new Error(`❌ Model "${name}" not found.`); + + // Asociaciones y hooks de los modelos, si existen + if (model.associate) { + model.associate(sequelize); + } + + if (model.hooks) { + model.hooks(sequelize); + } + + initializedModels.add(name); + logger.info(`🔸 Model "${model.name}" registered (sequelize)`); +}; diff --git a/apps/server/src/core/helpers/module-loader.ts b/apps/server/src/core/helpers/module-loader.ts new file mode 100644 index 0000000..5e9b999 --- /dev/null +++ b/apps/server/src/core/helpers/module-loader.ts @@ -0,0 +1,55 @@ +import { logger } from "@rdx/logger"; +import { IModuleServer } from "@rdx/modules"; +import { Application } from "express"; +import { Sequelize } from "sequelize"; +import { initModels, registerModel } from "./model-loader"; +import { registerService } from "./service-registry"; + +const registeredModules: Map = new Map(); +const initializedModules = new Set(); + +export function registerModule(pkg: IModuleServer) { + if (registeredModules.has(pkg.metadata.name)) { + throw new Error(`❌ Paquete "${pkg.metadata.name}" ya registrado.`); + } + registeredModules.set(pkg.metadata.name, pkg); +} + +export function initModules(app: Application, database: Sequelize) { + registeredModules.forEach((_, name) => { + loadModule(name, app, database); + }); + initModels(database); +} + +const loadModule = (name: string, app: Application, database: Sequelize) => { + if (initializedModules.has(name)) return; + + const pkg = registeredModules.get(name); + if (!pkg) throw new Error(`❌ Paquete "${name}" no encontrado.`); + + // Resolver dependencias primero + const deps = pkg.metadata.dependencies || []; + deps.forEach((dep) => loadModule(dep, app, database)); + + // Inicializar el module + pkg.init(app); + + const pkgApi = pkg.registerDependencies?.(); + + // Registrar modelos de Sequelize, si los expone + if (pkgApi?.models) { + registerModel(pkgApi.models, database); + } + + // Registrar sus servicios, si los expone + if (pkgApi?.services) { + const services = pkgApi.services; + if (services && typeof services === "object") { + registerService(pkg.metadata.name, services); + } + } + + initializedModules.add(name); + logger.info(`✅ Paquete "${name}" registrado.`); +}; diff --git a/apps/server/src/core/helpers/service-registry.ts b/apps/server/src/core/helpers/service-registry.ts new file mode 100644 index 0000000..133b92d --- /dev/null +++ b/apps/server/src/core/helpers/service-registry.ts @@ -0,0 +1,36 @@ +const services: Record = {}; + +/** + * Registra un objeto de servicio (API) bajo un nombre. + */ +export function registerService(name: string, api: any) { + if (services[name]) { + throw new Error(`❌ Servicio "${name}" ya fue registrado.`); + } + services[name] = api; +} + +/** + * Recupera un servicio registrado, con tipado opcional. + */ +export function getService(name: string): T { + const service = services[name]; + if (!service) { + throw new Error(`❌ Servicio "${name}" no encontrado.`); + } + return service; +} + +/** + * Permite saber si un servicio fue registrado. + */ +export function hasService(name: string): boolean { + return !!services[name]; +} + +/** + * Devuelve todos los servicios (para depuración o tests). + */ +export function listServices(): string[] { + return Object.keys(services); +} diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts new file mode 100644 index 0000000..acf4591 --- /dev/null +++ b/apps/server/src/index.ts @@ -0,0 +1,137 @@ +import { logger } from "@rdx/logger"; +import http from "http"; +import { DateTime } from "luxon"; +import { createApp } from "./app"; +import { ENV } from "./config"; +import { connectToDatabase, sequelize } from "./config/database"; +import { initModules } from "./core/helpers"; +import { registerModules } from "./modules"; + +// Guardamos información del estado del servidor +export const currentState = { + launchedAt: DateTime.now(), + appPath: process.cwd(), + host: ENV.HOST, + port: ENV.PORT, + environment: ENV.NODE_ENV, + connections: {} as Record, +}; + +// Manejo de cierre forzado del servidor (graceful shutdown) +const serverStop = (server: http.Server) => { + const forceTimeout = 30000; + + return new Promise((resolve, reject) => { + logger.warn("⚡️ Shutting down server"); + + setTimeout(() => { + logger.error("Could not close connections in time, forcefully shutting down"); + resolve(); + }, forceTimeout).unref(); + + server.close((err) => { + if (err) { + return reject(err); + } + logger.info("Closed out remaining connections."); + logger.info("❎ Bye!"); + resolve(); + }); + }); +}; + +// Manejo de errores al iniciar el servidor +const serverError = (error: NodeJS.ErrnoException) => { + logger.info(`⛔️ Server wasn't able to start properly.`); + + if (error.code === "EADDRINUSE") { + logger.error(error.message); + //logger.error(`The port ${error.port} is already used by another application.`); + } else { + logger.error(error); + } + + // Dependiendo de la criticidad, podrías forzar el proceso a salir + process.exit(1); +}; + +// Almacena en "connections" cada nueva conexión (descomentar si se quiere seguimiento) +const serverConnection = (conn: any) => { + const key = `${conn.remoteAddress}:${conn.remotePort}`; + currentState.connections[key] = conn; + + conn.on("close", () => { + delete currentState.connections[key]; + }); +}; + +//const sequelizeConn = createSequelizeAdapter(); +//const firebirdConn = createFirebirdAdapter(); + +// Registrar paquetes de la aplicación +registerModules(); + +const app = createApp(); + +// Crea el servidor HTTP +const server = http + .createServer(app) + .once("listening", () => + process.on("SIGINT", async () => { + // Por ejemplo, podrías desconectar la base de datos aquí: + // firebirdConn.disconnect(); + // O forzar desconexión en adapters + // sequelizeConn.close(); + + await serverStop(server); + }) + ) + .on("close", () => + logger.info(`Shut down at: ${DateTime.now().toLocaleString(DateTime.DATETIME_FULL)}`) + ) + .on("connection", serverConnection) + .on("error", serverError); + +// Ejemplo de adapters de base de datos (descoméntalos si los necesitas) +// const sequelizeConn = createSequelizeAdapter(); +// const firebirdConn = createFirebirdAdapter(); + +// Manejo de promesas no capturadas +process.on("unhandledRejection", (reason: any, promise: Promise) => { + logger.error("❌ Unhandled rejection at:", promise, "reason:", reason); + // Dependiendo de la aplicación, podrías desear una salida total o un cierre controlado + process.exit(1); +}); + +// Manejo de excepciones no controladas +process.on("uncaughtException", (error: Error) => { + // firebirdConn.disconnect(); + logger.error(`❌ Uncaught exception:`, error.message); + logger.error(error.stack); + // process.exit(1); +}); + +// Arranca el servidor si la conexión a la base de datos va bien +(async (app) => { + try { + const now = DateTime.now(); + logger.info(`Time: ${now.toLocaleString(DateTime.DATETIME_FULL)} ${now.zoneName}`); + logger.info(`Launched in: ${now.diff(currentState.launchedAt).toMillis()} ms`); + logger.info(`Environment: ${currentState.environment}`); + logger.info(`Process PID: ${process.pid}`); + + await connectToDatabase(); + // Lógica de inicialización de DB, si procede: + // initStructure(sequelizeConn.connection); + // insertUsers(); + + initModules(app, sequelize); + + server.listen(currentState.port, () => { + logger.info("To shut down your server, press + C at any time"); + logger.info(`⚡️ Server: http://${currentState.host}:${currentState.port}`); + }); + } catch (error) { + serverError(error as NodeJS.ErrnoException); + } +})(app); diff --git a/apps/server/src/modules.ts b/apps/server/src/modules.ts new file mode 100644 index 0000000..6ebe1cc --- /dev/null +++ b/apps/server/src/modules.ts @@ -0,0 +1,9 @@ +//import { ContactsPackage } from '@modules/contacts/server'; +import { invoicesModule } from "@modules/invoices"; +import { registerModule } from "./core/helpers"; + +export const registerModules = () => { + //registerPackage(ContactsPackage); + registerModule(invoicesModule); + //registerModule(); +}; diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json new file mode 100644 index 0000000..1a6fa98 --- /dev/null +++ b/apps/server/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["node", "jest"], + + "baseUrl": "./", + "paths": { + "@/*": ["./src/*"] + } + }, + "files": ["src/index.ts"], + "include": ["src/index.ts"], + "exclude": ["node_modules", "dist", "**/*/__tests__"] +} diff --git a/apps/web/.eslintrc.cjs b/apps/web/.eslintrc.cjs new file mode 100644 index 0000000..7b60203 --- /dev/null +++ b/apps/web/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["@repo/eslint-config/vite.js"], +}; diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..dec270e --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..1409e0a --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --clearScreen false", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint \"src/**/*.ts\"" + }, + "dependencies": { + "@repo/ui": "workspace:*", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "eslint": "^9.25.1", + "typescript": "5.8.3", + "vite": "^6.3.3" + } +} diff --git a/apps/web/public/typescript.svg b/apps/web/public/typescript.svg new file mode 100644 index 0000000..d91c910 --- /dev/null +++ b/apps/web/public/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/vite.svg b/apps/web/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/apps/web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/main.d.ts b/apps/web/src/main.d.ts new file mode 100644 index 0000000..6ec52ba --- /dev/null +++ b/apps/web/src/main.d.ts @@ -0,0 +1,2 @@ +import "./style.css"; +//# sourceMappingURL=main.d.ts.map \ No newline at end of file diff --git a/apps/web/src/main.d.ts.map b/apps/web/src/main.d.ts.map new file mode 100644 index 0000000..ea4e2a2 --- /dev/null +++ b/apps/web/src/main.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["main.tsx"],"names":[],"mappings":"AACA,OAAO,aAAa,CAAC"} \ No newline at end of file diff --git a/apps/web/src/main.js b/apps/web/src/main.js new file mode 100644 index 0000000..82cf1c0 --- /dev/null +++ b/apps/web/src/main.js @@ -0,0 +1,7 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { createRoot } from "react-dom/client"; +import "./style.css"; +import typescriptLogo from "/typescript.svg"; +import { Header, Counter } from "@repo/ui"; +var App = function () { return (_jsxs("div", { children: [_jsx("a", { href: "https://vitejs.dev", target: "_blank", children: _jsx("img", { src: "/vite.svg", className: "logo", alt: "Vite logo" }) }), _jsx("a", { href: "https://www.typescriptlang.org/", target: "_blank", children: _jsx("img", { src: typescriptLogo, className: "logo vanilla", alt: "TypeScript logo" }) }), _jsx(Header, { title: "Web" }), _jsx("div", { className: "card", children: _jsx(Counter, {}) })] })); }; +createRoot(document.getElementById("app")).render(_jsx(App, {})); diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..2af81b7 --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,25 @@ +import { createRoot } from "react-dom/client"; +import "./style.css"; +import typescriptLogo from "/typescript.svg"; +import { Header, Counter } from "@repo/ui"; + +const App = () => ( +
+ + Vite logo + + + TypeScript logo + +
+
+ +
+
+); + +createRoot(document.getElementById("app")!).render(); diff --git a/apps/web/src/style.css b/apps/web/src/style.css new file mode 100644 index 0000000..1232080 --- /dev/null +++ b/apps/web/src/style.css @@ -0,0 +1,97 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..9e7332d --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@repo/typescript-config/vite.json", + "include": ["src"], + "compilerOptions": { + "jsx": "react-jsx" + } +} \ No newline at end of file diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..081c8d9 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], +}); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d60ed4c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +version: "3" + +services: + web: + container_name: web + build: + context: . + dockerfile: ./apps/web/Dockerfile + restart: always + ports: + - 3000:3000 + networks: + - app_network + api: + container_name: api + build: + context: . + dockerfile: ./apps/api/Dockerfile + restart: always + ports: + - 3001:3001 + networks: + - app_network + +# Define a network, which allows containers to communicate +# with each other, by using their container name as a hostname +networks: + app_network: + external: true diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..946a1b9 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,422 @@ +## **Diseño de API RESTful** + +### **1. Autenticación y Gestión de Sesiones** + +#### **1.1. Inicio de sesión** + +**Endpoint:** +`POST /api/auth/login` + +**Solicitud:** + +```json +{ + "email": "usuario@empresa.com", + "password": "contraseña_segura" +} +``` + +**Respuesta:** + +```json +{ + "token": "eyJhbGciOi...", + "user": { + "id": 123, + "username": "usuario", + "email": "usuario@empresa.com" + }, + "companies": [ + { "id": 1, "name": "Empresa A" }, + { "id": 2, "name": "Empresa B" } + ] +} +``` + +--- + +#### **1.2. Cierre de sesión** + +**Endpoint:** +`POST /api/auth/logout` + +**Encabezados:** + +``` +Authorization: Bearer +``` + +**Respuesta:** + +```json +{ + "message": "Sesión cerrada exitosamente" +} +``` + +--- + +#### **1.3. Selección de empresa activa** + +**Endpoint:** +`POST /api/auth/select-company` + +**Solicitud:** + +```json +{ + "company_id": 1 +} +``` + +**Respuesta:** + +```json +{ + "message": "Empresa seleccionada exitosamente" +} +``` + +--- + +### **2. Gestión de Empresas** + +#### **2.1. Listar empresas disponibles** + +**Endpoint:** +`GET /api/companies` + +**Encabezados:** + +``` +Authorization: Bearer +``` + +**Respuesta:** + +```json +[ + { + "id": 1, + "name": "Empresa A", + "country_code": "ES", + "currency_code": "EUR" + }, + { "id": 2, "name": "Empresa B", "country_code": "US", "currency_code": "USD" } +] +``` + +--- + +#### **2.2. Crear una nueva empresa** + +**Endpoint:** +`POST /api/companies` + +**Solicitud:** + +```json +{ + "name": "Nueva Empresa", + "country_code": "ES", + "currency_code": "EUR" +} +``` + +**Respuesta:** + +```json +{ + "id": 3, + "message": "Empresa creada exitosamente" +} +``` + +--- + +#### **2.3. Actualizar datos de una empresa** + +**Endpoint:** +`PUT /api/companies/{company_id}` + +**Solicitud:** + +```json +{ + "name": "Empresa Actualizada", + "currency_code": "USD" +} +``` + +**Respuesta:** + +```json +{ + "message": "Empresa actualizada correctamente" +} +``` + +--- + +#### **2.4. Eliminar una empresa** + +**Endpoint:** +`DELETE /api/companies/{company_id}` + +**Respuesta:** + +```json +{ + "message": "Empresa eliminada exitosamente" +} +``` + +--- + +### **3. Gestión de Sucursales** + +#### **3.1. Listar sucursales de una empresa** + +**Endpoint:** +`GET /api/companies/{company_id}/branches` + +**Respuesta:** + +```json +[ + { "id": 10, "name": "Sucursal Madrid" }, + { "id": 20, "name": "Sucursal Barcelona" } +] +``` + +--- + +#### **3.2. Crear una sucursal** + +**Endpoint:** +`POST /api/companies/{company_id}/branches` + +**Solicitud:** + +```json +{ + "name": "Sucursal Sevilla", + "location": "Avenida Principal 123" +} +``` + +**Respuesta:** + +```json +{ + "id": 30, + "message": "Sucursal creada exitosamente" +} +``` + +--- + +#### **3.3. Actualizar datos de una sucursal** + +**Endpoint:** +`PUT /api/branches/{branch_id}` + +**Solicitud:** + +```json +{ + "name": "Sucursal Actualizada", + "location": "Calle Nueva 456" +} +``` + +**Respuesta:** + +```json +{ + "message": "Sucursal actualizada correctamente" +} +``` + +--- + +#### **3.4. Eliminar una sucursal** + +**Endpoint:** +`DELETE /api/branches/{branch_id}` + +**Respuesta:** + +```json +{ + "message": "Sucursal eliminada exitosamente" +} +``` + +--- + +### **4. Gestión de Usuarios** + +#### **4.1. Listar usuarios de una empresa** + +**Endpoint:** +`GET /api/companies/{company_id}/users` + +**Respuesta:** + +```json +[ + { + "id": 1, + "username": "admin", + "email": "admin@empresa.com", + "roles": ["Administrador"] + }, + { + "id": 2, + "username": "usuario", + "email": "usuario@empresa.com", + "roles": ["Ventas"] + } +] +``` + +--- + +#### **4.2. Crear un usuario** + +**Endpoint:** +`POST /api/users` + +**Solicitud:** + +```json +{ + "username": "nuevo_usuario", + "email": "nuevo@empresa.com", + "password": "clave_segura", + "company_id": 1, + "roles": ["Ventas"] +} +``` + +**Respuesta:** + +```json +{ + "id": 3, + "message": "Usuario creado exitosamente" +} +``` + +--- + +#### **4.3. Actualizar usuario** + +**Endpoint:** +`PUT /api/users/{user_id}` + +**Solicitud:** + +```json +{ + "email": "nuevo_correo@empresa.com", + "roles": ["Compras"] +} +``` + +**Respuesta:** + +```json +{ + "message": "Usuario actualizado correctamente" +} +``` + +--- + +#### **4.4. Eliminar usuario** + +**Endpoint:** +`DELETE /api/users/{user_id}` + +**Respuesta:** + +```json +{ + "message": "Usuario eliminado exitosamente" +} +``` + +--- + +### **5. Gestión de Permisos** + +#### **5.1. Obtener permisos de un usuario en una empresa** + +**Endpoint:** +`GET /api/companies/{company_id}/users/{user_id}/permissions` + +**Respuesta:** + +```json +{ + "modules": { + "Presupuestos": ["read", "write"], + "Facturas": ["read"], + "Clientes": ["read", "write", "delete"] + } +} +``` + +--- + +#### **5.2. Actualizar permisos de un usuario** + +**Endpoint:** +`PUT /api/users/{user_id}/permissions` + +**Solicitud:** + +```json +{ + "modules": { + "Presupuestos": ["read", "write"], + "Inventario": ["read"] + } +} +``` + +**Respuesta:** + +```json +{ + "message": "Permisos actualizados correctamente" +} +``` + +--- + +### **6. Auditoría** + +#### **6.1. Obtener historial de cambios en documentos** + +**Endpoint:** +`GET /api/audit/documents/{document_id}` + +**Respuesta:** + +```json +[ + { + "timestamp": "2025-01-22T14:00:00Z", + "user": "admin", + "change": "Se modificó el precio de 100 a 120" + }, + { + "timestamp": "2025-01-20T10:30:00Z", + "user": "usuario", + "change": "Se creó el documento" + } +] +``` diff --git a/docs/DsRegistroVeriFactu.xlsx b/docs/DsRegistroVeriFactu.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b1a2e453ca4f5bb92b5fd10b339bf2569832c5db GIT binary patch literal 59877 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Π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

nOP?t18}S7OJB?s7Of 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 literal 0 HcmV?d00001 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4b76073 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,84 @@ +# Turborepo starter + +This Turborepo starter is maintained by the Turborepo core team. + +## Using this example + +Run the following command: + +```sh +npx create-turbo@latest +``` + +## What's inside? + +This Turborepo includes the following packages/apps: + +### Apps and Packages + +- `docs`: a [Next.js](https://nextjs.org/) app +- `web`: another [Next.js](https://nextjs.org/) app +- `@repo/ui`: a stub React component library shared by both `web` and `docs` applications +- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) +- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo + +Each package/app is 100% [TypeScript](https://www.typescriptlang.org/). + +### Utilities + +This Turborepo has some additional tools already setup for you: + +- [TypeScript](https://www.typescriptlang.org/) for static type checking +- [ESLint](https://eslint.org/) for code linting +- [Prettier](https://prettier.io) for code formatting + +### Build + +To build all apps and packages, run the following command: + +``` +cd my-turborepo +pnpm build +``` + +### Develop + +To develop all apps and packages, run the following command: + +``` +cd my-turborepo +pnpm dev +``` + +### Remote Caching + +> [!TIP] +> Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache). + +Turborepo can use a technique known as [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines. + +By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands: + +``` +cd my-turborepo +npx turbo login +``` + +This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview). + +Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo: + +``` +npx turbo link +``` + +## Useful Links + +Learn more about the power of Turborepo: + +- [Tasks](https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks) +- [Caching](https://turbo.build/repo/docs/core-concepts/caching) +- [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) +- [Filtering](https://turbo.build/repo/docs/core-concepts/monorepos/filtering) +- [Configuration Options](https://turbo.build/repo/docs/reference/configuration) +- [CLI Usage](https://turbo.build/repo/docs/reference/command-line-reference) diff --git a/docs/REQUISITOS CLIENTES.md b/docs/REQUISITOS CLIENTES.md new file mode 100644 index 0000000..0175fda --- /dev/null +++ b/docs/REQUISITOS CLIENTES.md @@ -0,0 +1,165 @@ +# **Especificaciones del Módulo de Clientes - ERP** + +El **módulo de clientes** del ERP permite gestionar la información de clientes, asegurando compatibilidad con la gestión multiempresa, multiidioma y multi-moneda. A continuación, se detallan las especificaciones, incluyendo los campos clave y su función. + +--- + +## **1. Funcionalidades del Módulo de Clientes** +El módulo de clientes debe permitir: +- **Crear, leer, actualizar y eliminar clientes** (CRUD). +- **Asociar clientes a una empresa** y, opcionalmente, a una o más sucursales. +- **Registrar clientes como personas físicas o empresas**. +- **Gestionar datos fiscales, comerciales y de contacto**. +- **Definir configuraciones de facturación y pago personalizadas**. +- **Aplicar descuentos en distintos niveles**. +- **Manejar aspectos financieros como retenciones y recargos**. +- **Identificar clientes con riesgo financiero**. +- **Asignar comerciales o delegados**. +- **Controlar el estado del cliente** (activo/inactivo). +- **Registrar auditoría de cambios**. + +--- + +## **2. Estructura del Cliente y sus Campos** +Cada cliente tiene los siguientes atributos: + +### **Datos Generales** +| Campo | Tipo de Dato | Descripción | +|-----------------------|-----------------|-------------| +| `id` | `INT` (PK) | Identificador único del cliente. | +| `company_id` | `INT` (FK) | Empresa a la que pertenece el cliente. | +| `is_company` | `BOOLEAN` | Indica si el cliente es una empresa (`true`) o una persona física (`false`). | +| `fiscal_name` | `VARCHAR(255)` | Nombre fiscal del cliente (solo si es empresa). | +| `commercial_name` | `VARCHAR(255)` | Nombre comercial del cliente (si aplica). | +| `name` | `VARCHAR(255)` | Nombre de la persona o empresa. | +| `email` | `VARCHAR(100)` | Correo electrónico único del cliente. | +| `phone` | `VARCHAR(20)` | Número de teléfono de contacto. | +| `address` | `TEXT` | Dirección del cliente. | +| `country_code` | `CHAR(2)` | Código del país del cliente (ISO 3166-1 alpha-2). | +| `origin` | `VARCHAR(100)` | Origen del cliente (ej. publicidad, redes sociales, feria, escaparate). | + +### **Configuración de Moneda e Idioma** +| Campo | Tipo de Dato | Descripción | +|---------------------|-----------------|-------------| +| `currency_code` | `CHAR(3)` | Código de la divisa con la que trabaja el cliente (ISO 4217). | +| `language_code` | `CHAR(5)` | Idioma preferido del cliente para documentos y comunicación (ej. `es-ES`, `en-US`). | + +### **Información Fiscal y Financiera** +| Campo | Tipo de Dato | Descripción | +|------------------------|----------------|-------------| +| `vat_percentage` | `DECIMAL(5,2)` | Porcentaje de IVA aplicable al cliente. | +| `equivalence_charge` | `BOOLEAN` | Indica si el cliente aplica recargo de equivalencia. | +| `withholding_percentage` | `DECIMAL(5,2)` | Porcentaje de retención en facturas del cliente. | +| `payment_method` | `VARCHAR(100)` | Forma de pago habitual del cliente (ej. transferencia, tarjeta, efectivo). | +| `payment_day` | `INT` | Día del mes en que el cliente realiza pagos (1-31). | +| `risk` | `BOOLEAN` | Indica si el cliente tiene riesgo financiero o historial de morosidad. | + +### **Descuentos y Tarifas Especiales** +| Campo | Tipo de Dato | Descripción | +|---------------------|----------------|-------------| +| `discount_line` | `DECIMAL(5,2)` | Descuento aplicado a nivel de línea en presupuestos. | +| `discount_chapter` | `DECIMAL(5,2)` | Descuento aplicado a nivel de capítulo en presupuestos. | +| `discount_global` | `DECIMAL(5,2)` | Descuento global aplicado al presupuesto. | +| `price_point` | `DECIMAL(10,2)` | Valor de "precio punto" del cliente, usado para calcular costos personalizados en catálogo. | + +### **Clasificación y Estado** +| Campo | Tipo de Dato | Descripción | +|---------------|----------------|-------------| +| `client_type` | `VARCHAR(100)` | Tipo de cliente (ej. profesionales, constructoras, distribuidores, particulares). | +| `is_active` | `BOOLEAN` | Indica si el cliente está activo (`true`) o dado de baja (`false`). | + +### **Delegados y Comerciales** +| Campo | Tipo de Dato | Descripción | +|-----------------|----------------|-------------| +| `Client_Sales_Rep` | `Tabla Relacional` | Relación con los comerciales asignados a este cliente. | + +--- + +## **3. Auditoría y Seguridad** +Cada cambio en los datos de un cliente debe registrarse en una tabla de auditoría: +```sql +CREATE TABLE Client_Audit ( + id SERIAL PRIMARY KEY, + client_id INT NOT NULL REFERENCES Clients(id) ON DELETE CASCADE, + user_id INT NOT NULL REFERENCES Users(id), + change_description TEXT, + change_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` +Esto permitirá rastrear modificaciones y garantizar transparencia en la gestión de clientes. + +--- + +## **4. Endpoints Principales de la API** +### **4.1. Listar Clientes** +```http +GET /api/companies/{company_id}/clients +Authorization: Bearer +``` + +### **4.2. Crear Cliente** +```http +POST /api/companies/{company_id}/clients +Authorization: Bearer +``` +**Solicitud:** +```json +{ + "is_company": true, + "fiscal_name": "Empresa XYZ SL", + "commercial_name": "XYZ Comercial", + "name": "Empresa XYZ", + "email": "contacto@xyz.com", + "phone": "+34912345678", + "country_code": "ES", + "origin": "Publicidad", + "currency_code": "EUR", + "language_code": "es-ES", + "vat_percentage": 21.00, + "payment_method": "Transferencia bancaria", + "discount_line": 5.00, + "discount_chapter": 2.50, + "discount_global": 10.00, + "price_point": 1.20, + "payment_day": 15, + "risk": false, + "equivalence_charge": false, + "withholding_percentage": 10.00, + "client_type": "Distribuidor", + "is_active": true +} +``` + +### **4.3. Obtener Cliente por ID** +```http +GET /api/clients/{client_id} +Authorization: Bearer +``` + +### **4.4. Actualizar Cliente** +```http +PUT /api/clients/{client_id} +Authorization: Bearer +``` + +### **4.5. Baja Lógica de Cliente** +```http +PATCH /api/clients/{client_id}/deactivate +Authorization: Bearer +``` + +### **4.6. Asignar Comercial a Cliente** +```http +POST /api/clients/{client_id}/sales-reps +Authorization: Bearer +``` + +--- + +## **5. Seguridad y Control de Acceso** +- **Middleware de autenticación** para validar que solo usuarios autorizados gestionen clientes. +- **Permisos basados en roles**, como `Clientes: leer`, `Clientes: escribir`, `Clientes: eliminar`. +- **Reglas de validación** para evitar datos erróneos o incompletos. +- **Registro de auditoría** para cada modificación. + +--- diff --git a/docs/REQUISITOS GENERALES.md b/docs/REQUISITOS GENERALES.md new file mode 100644 index 0000000..174659e --- /dev/null +++ b/docs/REQUISITOS GENERALES.md @@ -0,0 +1,207 @@ +# **Requisitos Técnicos del ERP** + +## **1. Administración de Usuarios** + +- **Roles y Perfiles**: + + - Los usuarios pueden tener uno o más perfiles predefinidos (por ejemplo, "Ventas", "Compras"). + - Los perfiles controlan permisos generales a nivel de empresa y refinados a nivel de sucursal. + - Un usuario puede tener diferentes perfiles en distintas sucursales de la misma empresa. + +- **Permisos**: + + - Configurables por módulo (Presupuestos, Facturas, etc.). + - Granularidad: permitir acciones específicas como "leer", "escribir", "eliminar". + - Un usuario puede tener permisos para acceder a una o más sucursales de la empresa, pero debe trabajar en una empresa activa por sesión. + - Si el usuario solo tiene permisos para una sucursal, se selecciona automáticamente. Si tiene acceso a varias, se selecciona solo cuando sea necesario. + +- **Superadministrador**: + - Puede crear, modificar y eliminar usuarios. + - Gestiona perfiles, permisos y asociaciones con empresas y sucursales. + +--- + +## **2. Gestión Multiempresa y Multisucursal** + +- **Usuarios y Empresas**: + + - Un usuario puede estar asociado a múltiples empresas. + - En cada sesión o pestaña, el usuario puede trabajar con una empresa específica. + - La empresa activa se elige al inicio de sesión. + +- **Sucursales**: + + - Cada empresa debe tener al menos una sucursal, pero puede tener varias. + - Los usuarios de una sucursal pueden gestionar datos de otras sucursales si tienen los permisos adecuados. + - Se permite definir permisos para operar en múltiples sucursales dentro de la misma empresa. + +- **Flujo de Selección**: + - Al iniciar sesión, el usuario elige la empresa activa. + - Si tiene acceso a una única sucursal, esta se selecciona automáticamente. + - Si tiene acceso a múltiples sucursales, la selección de sucursal ocurre solo cuando sea necesario para una operación específica. + +--- + +## **3. Soporte Multiidioma y Regionalización** + +- **Idiomas**: + + - La interfaz del ERP debe estar disponible en varios idiomas. + - Los documentos (presupuestos, facturas) pueden generarse en el idioma del cliente. + - El idioma de usuario es configurable en su perfil. + +- **Monedas e Impuestos**: + - Configuración de moneda por empresa, con posibilidad de conversión. + - Definición de impuestos según país (IVA, retenciones, tasas locales). + - Flexibilidad para configurar tarifas por categoría de productos o servicios. + +--- + +## **4. Contexto de Sesión y Pestaña** + +- **Contexto por Pestaña**: + + - Cada pestaña tiene un contexto independiente (`tab_id`), que incluye la empresa activa. + - Si se requiere una sucursal para la operación, se solicitará en ese momento. + - Se almacena el `tab_id` junto con la empresa activa en el backend para cada sesión de usuario. + +- **Gestión de Sesiones**: + - Tokens JWT deben incluir información sobre: + - `user_id` + - `tab_id` + - `active_company_id` + - Lista de permisos específicos + - Se debe registrar el contexto de pestaña en la base de datos para auditoría y trazabilidad. + +--- + +## **5. Auditoría y Registro de Actividades** + +- **Registro de Cambios en Documentos**: + + - Presupuestos, facturas y otros documentos deben mantener un historial detallado de modificaciones. + - Se registra la siguiente información: + - Usuario que realizó el cambio. + - Fecha y hora. + - Cambios específicos (antes y después). + +- **Trazabilidad Global**: + - Auditar las actividades del usuario a nivel de empresa, sucursal y pestaña. + - Se debe asociar cada acción al `tab_id` para identificar actividades por pestaña específica. + +--- + +## **6. Documentos Operativos** + +- **Historial de Cambios**: + + - Cada documento (presupuestos, facturas, etc.) debe registrar todas las modificaciones con la posibilidad de revertirlas si es necesario. + +- **Exportación Multiidioma**: + - Generación de documentos en diferentes idiomas según la configuración del cliente. + +--- + +## **7. Seguridad** + +- **Autenticación**: + + - Autenticación basada en tokens JWT. + - Contraseñas encriptadas con bcrypt o Argon2. + - Soporte para autenticación en dos pasos (2FA) en futuras iteraciones. + +- **Control de Acceso**: + - Validación de permisos en cada solicitud al backend. + - Aplicación del principio de mínimos privilegios para usuarios. + +--- + +## **8. Extensibilidad y Modularidad** + +- **Estructura Modular**: + - El sistema debe permitir la integración de nuevos módulos sin afectar los permisos o el funcionamiento existente. + - Módulos actuales planificados: + - Presupuestos + - Facturas + - Clientes + - Pedidos + - Inventario (futuro) + - Recursos Humanos (futuro) + +--- + +## **9. Base de Datos - Diseño Técnico** + +### **Tablas Principales** + +```sql +-- Usuarios +CREATE TABLE Users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +``` + +```sql +-- Empresas +CREATE TABLE Companies ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) UNIQUE NOT NULL, + country_code CHAR(2), + currency_code CHAR(3), + tax_config JSONB, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +``` + +```sql +-- Sucursales +CREATE TABLE Branches ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + company_id INT NOT NULL REFERENCES Companies(id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +``` + +```sql +-- Relación Usuario - Empresa +CREATE TABLE User_Company ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL REFERENCES Users(id), + company_id INT NOT NULL REFERENCES Companies(id), + is_active BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +``` + +```sql +-- Relación Usuario - Sucursal +CREATE TABLE User_Branch ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL REFERENCES Users(id), + branch_id INT NOT NULL REFERENCES Branches(id), + permissions JSONB NOT NULL +); +``` + +```sql +-- Contexto por pestaña +CREATE TABLE Tab_Context ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL REFERENCES Users(id), + tab_id UUID NOT NULL, + company_id INT NOT NULL REFERENCES Companies(id), + branch_id INT REFERENCES Branches(id) +); +``` + +--- diff --git a/modules/invoices/package.json b/modules/invoices/package.json new file mode 100644 index 0000000..4fd6b62 --- /dev/null +++ b/modules/invoices/package.json @@ -0,0 +1,81 @@ +{ + "name": "@modules/invoices", + "version": "0.0.0", + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsc --project tsconfig.json && tsc-alias -p tsconfig.json", + "clean": "rm -rf dist && rm -rf node_modules", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "jest": { + "preset": "@repo/jest-presets/node" + }, + "devDependencies": { + "@jest/globals": "^29.7.0", + "@repo/eslint-config": "workspace:*", + "@repo/jest-presets": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/bcrypt": "^5.0.2", + "@types/body-parser": "^1.19.5", + "@types/cors": "^2.8.17", + "@types/dinero.js": "^1.9.4", + "@types/express": "^4.17.21", + "@types/glob": "^8.1.0", + "@types/jest": "^29.5.14", + "@types/jsonwebtoken": "^9.0.9", + "@types/luxon": "^3.6.2", + "@types/morgan": "^1.9.9", + "@types/node": "^22.15.2", + "@types/passport": "^1.0.17", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", + "@types/response-time": "^2.3.8", + "@types/supertest": "^6.0.3", + "@typescript-eslint/eslint-plugin": "^8.31.0", + "@typescript-eslint/parser": "^8.31.0", + "esbuild": "^0.25.3", + "esbuild-register": "^3.6.0", + "eslint": "^9.25.1", + "jest": "^29.7.0", + "nodemon": "^3.1.10", + "supertest": "^7.1.0", + "tsc-alias": "^1.8.15", + "typescript": "5.8.3" + }, + "dependencies": { + "@rdx/core": "workspace:*", + "@rdx/ddd-domain": "workspace:*", + "@rdx/logger": "workspace:*", + "@rdx/modules": "workspace:*", + "@rdx/utils": "workspace:*", + "bcrypt": "^5.1.1", + "body-parser": "^2.2.0", + "cors": "^2.8.5", + "dinero.js": "^1.9.1", + "dotenv": "^16.5.0", + "express": "^4.21.2", + "helmet": "^8.1.0", + "http-status": "^2.1.0", + "jsonwebtoken": "^9.0.2", + "libphonenumber-js": "^1.11.20", + "luxon": "^3.5.0", + "module-alias": "^2.2.3", + "mysql2": "^3.12.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", + "path": "^0.12.7", + "reflect-metadata": "^0.2.2", + "response-time": "^2.3.3", + "sequelize": "^6.37.7", + "zod": "^3.24.3" + } +} diff --git a/modules/invoices/src/client/index.ts b/modules/invoices/src/client/index.ts new file mode 100644 index 0000000..2d1ec23 --- /dev/null +++ b/modules/invoices/src/client/index.ts @@ -0,0 +1 @@ +export default () => {}; diff --git a/modules/invoices/src/index.ts b/modules/invoices/src/index.ts new file mode 100644 index 0000000..a15f050 --- /dev/null +++ b/modules/invoices/src/index.ts @@ -0,0 +1,2 @@ +//export * from "./client"; +export * from "./server"; diff --git a/modules/invoices/src/server/application/create-invoice.use-case.ts b/modules/invoices/src/server/application/create-invoice.use-case.ts new file mode 100644 index 0000000..775db83 --- /dev/null +++ b/modules/invoices/src/server/application/create-invoice.use-case.ts @@ -0,0 +1,141 @@ +import { ITransactionManager } from "@rdx/core"; +import { UniqueID, UtcDate } from "@rdx/ddd-domain"; +import { logger } from "@rdx/logger"; +import { Result } from "@rdx/utils"; +import { IInvoiceService, Invoice } from "../domain"; + +import { IInvoiceProps, InvoiceNumber, InvoiceSerie, InvoiceStatus } from "../domain"; +import { ICreateInvoiceRequestDTO } from "../presentation"; + +export class CreateInvoiceUseCase { + constructor( + private readonly invoiceService: IInvoiceService, + private readonly transactionManager: ITransactionManager + ) {} + + public execute( + invoiceID: UniqueID, + dto: ICreateInvoiceRequestDTO + ): Promise> { + return this.transactionManager.complete(async (transaction) => { + try { + const validOrErrors = this.validateInvoiceData(dto); + if (validOrErrors.isFailure) { + return Result.fail(validOrErrors.error); + } + + const data = validOrErrors.data; + + // Update invoice with dto + return await this.invoiceService.createInvoice(invoiceID, data, transaction); + } catch (error: unknown) { + logger.error(error as Error); + return Result.fail(error as Error); + } + }); + } + + private validateInvoiceData(dto: ICreateInvoiceRequestDTO): Result { + const errors: Error[] = []; + + const invoiceNumerOrError = InvoiceNumber.create(dto.invoice_number); + const invoiceSeriesOrError = InvoiceSerie.create(dto.invoice_series); + const issueDateOrError = UtcDate.create(dto.issue_date); + const operationDateOrError = UtcDate.create(dto.operation_date); + + const result = Result.combine([ + invoiceNumerOrError, + invoiceSeriesOrError, + issueDateOrError, + operationDateOrError, + ]); + + if (result.isFailure) { + return Result.fail(result.error); + } + + const validatedData: IInvoiceProps = { + status: InvoiceStatus.createDraft(), + invoiceNumber: invoiceNumerOrError.data, + invoiceSeries: invoiceSeriesOrError.data, + issueDate: issueDateOrError.data, + operationDate: operationDateOrError.data, + invoiceCurrency: dto.currency, + }; + + /*if (errors.length > 0) { + const message = errors.map((err) => err.message).toString(); + return Result.fail(new Error(message)); + }*/ + return Result.ok(validatedData); + + /*let invoice_status = InvoiceStatus.create(dto.status).object; + if (invoice_status.isEmpty()) { + invoice_status = InvoiceStatus.createDraft(); + } + + let invoice_series = InvoiceSeries.create(dto.invoice_series).object; + if (invoice_series.isEmpty()) { + invoice_series = InvoiceSeries.create(dto.invoice_series).object; + } + + let issue_date = InvoiceDate.create(dto.issue_date).object; + if (issue_date.isEmpty()) { + issue_date = InvoiceDate.createCurrentDate().object; + } + + let operation_date = InvoiceDate.create(dto.operation_date).object; + if (operation_date.isEmpty()) { + operation_date = InvoiceDate.createCurrentDate().object; + } + + let invoiceCurrency = Currency.createFromCode(dto.currency).object; + + if (invoiceCurrency.isEmpty()) { + invoiceCurrency = Currency.createDefaultCode().object; + } + + let invoiceLanguage = Language.createFromCode(dto.language_code).object; + + if (invoiceLanguage.isEmpty()) { + invoiceLanguage = Language.createDefaultCode().object; + } + + const items = new Collection( + dto.items?.map( + (item) => + InvoiceSimpleItem.create({ + description: Description.create(item.description).object, + quantity: Quantity.create(item.quantity).object, + unitPrice: UnitPrice.create({ + amount: item.unit_price.amount, + currencyCode: item.unit_price.currency, + precision: item.unit_price.precision, + }).object, + }).object + ) + ); + + if (!invoice_status.isDraft()) { + throw Error("Error al crear una factura que no es borrador"); + } + + return DraftInvoice.create( + { + invoiceSeries: invoice_series, + issueDate: issue_date, + operationDate: operation_date, + invoiceCurrency, + language: invoiceLanguage, + invoiceNumber: InvoiceNumber.create(undefined).object, + //notes: Note.create(invoiceDTO.notes).object, + + //senderId: UniqueID.create(null).object, + recipient, + + items, + }, + invoiceId + );*/ + } +} diff --git a/modules/invoices/src/server/application/delete-invoice.use-case.ts b/modules/invoices/src/server/application/delete-invoice.use-case.ts new file mode 100644 index 0000000..fb91ed5 --- /dev/null +++ b/modules/invoices/src/server/application/delete-invoice.use-case.ts @@ -0,0 +1,23 @@ +import { ITransactionManager } from "@rdx/core"; +import { UniqueID } from "@rdx/ddd-domain"; +import { logger } from "@rdx/logger"; +import { Result } from "@rdx/utils"; +import { IInvoiceService } from "../domain"; + +export class DeleteInvoiceUseCase { + constructor( + private readonly invoiceService: IInvoiceService, + private readonly transactionManager: ITransactionManager + ) {} + + public execute(invoiceID: UniqueID): Promise> { + return this.transactionManager.complete(async (transaction) => { + try { + return await this.invoiceService.deleteInvoiceById(invoiceID, transaction); + } catch (error: unknown) { + logger.error(error as Error); + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/invoices/src/server/application/get-invoice.use-case.ts b/modules/invoices/src/server/application/get-invoice.use-case.ts new file mode 100644 index 0000000..224b027 --- /dev/null +++ b/modules/invoices/src/server/application/get-invoice.use-case.ts @@ -0,0 +1,23 @@ +import { ITransactionManager } from "@rdx/core"; +import { UniqueID } from "@rdx/ddd-domain"; +import { logger } from "@rdx/logger"; +import { Result } from "@rdx/utils"; +import { IInvoiceService, Invoice } from "../domain"; + +export class GetInvoiceUseCase { + constructor( + private readonly invoiceService: IInvoiceService, + private readonly transactionManager: ITransactionManager + ) {} + + public execute(invoiceID: UniqueID): Promise> { + return this.transactionManager.complete(async (transaction) => { + try { + return await this.invoiceService.findInvoiceById(invoiceID, transaction); + } catch (error: unknown) { + logger.error(error as Error); + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/invoices/src/server/application/index.ts b/modules/invoices/src/server/application/index.ts new file mode 100644 index 0000000..075752c --- /dev/null +++ b/modules/invoices/src/server/application/index.ts @@ -0,0 +1,5 @@ +export * from "./create-invoice.use-case"; +export * from "./delete-invoice.use-case"; +export * from "./get-invoice.use-case"; +export * from "./list-invoices.use-case"; +//export * from "./update-invoice.use-case"; diff --git a/modules/invoices/src/server/application/list-invoices.use-case.ts b/modules/invoices/src/server/application/list-invoices.use-case.ts new file mode 100644 index 0000000..b56db8e --- /dev/null +++ b/modules/invoices/src/server/application/list-invoices.use-case.ts @@ -0,0 +1,22 @@ +import { ITransactionManager } from "@rdx/core"; +import { logger } from "@rdx/logger"; +import { Collection, Result } from "@rdx/utils"; +import { IInvoiceService, Invoice } from "../domain"; + +export class ListInvoicesUseCase { + constructor( + private readonly invoiceService: IInvoiceService, + private readonly transactionManager: ITransactionManager + ) {} + + public execute(): Promise, Error>> { + return this.transactionManager.complete(async (transaction) => { + try { + return await this.invoiceService.findInvoices(transaction); + } catch (error: unknown) { + logger.error(error as Error); + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/invoices/src/server/application/services/index.ts b/modules/invoices/src/server/application/services/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/modules/invoices/src/server/application/services/participantAddressFinder.ts.bak b/modules/invoices/src/server/application/services/participantAddressFinder.ts.bak new file mode 100644 index 0000000..7a85cfa --- /dev/null +++ b/modules/invoices/src/server/application/services/participantAddressFinder.ts.bak @@ -0,0 +1,70 @@ +import { + ApplicationServiceError, + IApplicationServiceError, +} from "@/contexts/common/application/services/ApplicationServiceError"; +import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain"; +import { Result, UniqueID } from "@shared/contexts"; +import { NullOr } from "@shared/utilities"; +import { + IInvoiceParticipantAddress, + IInvoiceParticipantAddressRepository, +} from "../../domain"; + +export const participantAddressFinder = async ( + addressId: UniqueID, + adapter: IAdapter, + repository: RepositoryBuilder, +) => { + if (addressId.isNull()) { + return Result.fail( + ApplicationServiceError.create( + ApplicationServiceError.INVALID_REQUEST_PARAM, + `Participant address ID required`, + ), + ); + } + + const transaction = adapter.startTransaction(); + let address: NullOr = null; + + try { + await transaction.complete(async (t) => { + address = await repository({ transaction: t }).getById(addressId); + }); + + if (address === null) { + return Result.fail( + ApplicationServiceError.create( + ApplicationServiceError.NOT_FOUND_ERROR, + "", + { + id: addressId.toString(), + entity: "participant address", + }, + ), + ); + } + + return Result.ok(address); + } catch (error: unknown) { + const _error = error as Error; + + if (repository().isRepositoryError(_error)) { + return Result.fail( + ApplicationServiceError.create( + ApplicationServiceError.REPOSITORY_ERROR, + _error.message, + _error, + ), + ); + } + + return Result.fail( + ApplicationServiceError.create( + ApplicationServiceError.UNEXCEPTED_ERROR, + _error.message, + _error, + ), + ); + } +}; diff --git a/modules/invoices/src/server/application/services/participantFinder.ts.bak b/modules/invoices/src/server/application/services/participantFinder.ts.bak new file mode 100644 index 0000000..b73ad1c --- /dev/null +++ b/modules/invoices/src/server/application/services/participantFinder.ts.bak @@ -0,0 +1,20 @@ +import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain"; +import { UniqueID } from "@shared/contexts"; +import { IInvoiceParticipantRepository } from "../../domain"; +import { InvoiceCustomer } from "../../domain/entities/invoice-customer/invoice-customer"; + +export const participantFinder = async ( + participantId: UniqueID, + adapter: IAdapter, + repository: RepositoryBuilder +): Promise => { + if (!participantId || (participantId && participantId.isNull())) { + return Promise.resolve(undefined); + } + + const participant = await adapter + .startTransaction() + .complete((t) => repository({ transaction: t }).getById(participantId)); + + return Promise.resolve(participant ? participant : undefined); +}; diff --git a/modules/invoices/src/server/application/update-invoice.use-case.bak b/modules/invoices/src/server/application/update-invoice.use-case.bak new file mode 100644 index 0000000..e65b699 --- /dev/null +++ b/modules/invoices/src/server/application/update-invoice.use-case.bak @@ -0,0 +1,400 @@ +import { ITransactionManager } from "@rdx/core"; +import { UniqueID } from "@rdx/ddd-domain"; +import { logger } from "@rdx/logger"; +import { Collection, Result } from "@rdx/utils"; +import { IInvoiceService, Invoice, InvoiceItem } from "../domain"; + +import { IInvoiceProps, InvoiceNumber, InvoiceStatus } from "../domain"; +import { IUpdateInvoiceRequestDTO } from "../presentation/dto"; + +export class UpdateInvoiceUseCase { + constructor( + private readonly invoiceService: IInvoiceService, + private readonly transactionManager: ITransactionManager + ) {} + + public execute( + invoiceID: UniqueID, + dto: Partial + ): Promise> { + return this.transactionManager.complete(async (transaction) => { + try { + const validOrErrors = this.validateInvoiceData(dto); + if (validOrErrors.isFailure) { + return Result.fail(validOrErrors.error); + } + + const data = validOrErrors.data; + + // Update invoice with dto + return await this.invoiceService.updateInvoiceById(invoiceID, data, transaction); + } catch (error: unknown) { + logger.error(error as Error); + return Result.fail(error as Error); + } + }); + } + + private validateInvoiceData( + dto: Partial + ): Result, Error> { + const errors: Error[] = []; + const validatedData: Partial = {}; + + // Create invoice + let invoice_status = InvoiceStatus.create(invoiceDTO.status).object; + if (invoice_status.isEmpty()) { + invoice_status = InvoiceStatus.createDraft(); + } + + let invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object; + if (invoice_series.isEmpty()) { + invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object; + } + + let issue_date = InvoiceDate.create(invoiceDTO.issue_date).object; + if (issue_date.isEmpty()) { + issue_date = InvoiceDate.createCurrentDate().object; + } + + let operation_date = InvoiceDate.create(invoiceDTO.operation_date).object; + if (operation_date.isEmpty()) { + operation_date = InvoiceDate.createCurrentDate().object; + } + + let invoiceCurrency = Currency.createFromCode(invoiceDTO.currency).object; + + if (invoiceCurrency.isEmpty()) { + invoiceCurrency = Currency.createDefaultCode().object; + } + + let invoiceLanguage = Language.createFromCode(invoiceDTO.language_code).object; + + if (invoiceLanguage.isEmpty()) { + invoiceLanguage = Language.createDefaultCode().object; + } + + const items = new Collection( + invoiceDTO.items?.map( + (item) => + InvoiceSimpleItem.create({ + description: Description.create(item.description).object, + quantity: Quantity.create(item.quantity).object, + unitPrice: UnitPrice.create({ + amount: item.unit_price.amount, + currencyCode: item.unit_price.currency, + precision: item.unit_price.precision, + }).object, + }).object + ) + ); + + if (!invoice_status.isDraft()) { + throw Error("Error al crear una factura que no es borrador"); + } + + return DraftInvoice.create( + { + invoiceSeries: invoice_series, + issueDate: issue_date, + operationDate: operation_date, + invoiceCurrency, + language: invoiceLanguage, + invoiceNumber: InvoiceNumber.create(undefined).object, + //notes: Note.create(invoiceDTO.notes).object, + + //senderId: UniqueID.create(null).object, + recipient, + + items, + }, + invoiceId + ); + } +} + +export type UpdateInvoiceResponseOrError = + | Result // Misc errors (value objects) + | Result; // Success! + +export class UpdateInvoiceUseCase2 + implements + IUseCase<{ id: UniqueID; data: IUpdateInvoice_DTO }, Promise> +{ + private _context: IInvoicingContext; + private _adapter: ISequelizeAdapter; + private _repositoryManager: IRepositoryManager; + + constructor(context: IInvoicingContext) { + this._context = context; + this._adapter = context.adapter; + this._repositoryManager = context.repositoryManager; + } + + private getRepository(name: string) { + return this._repositoryManager.getRepository(name); + } + + private handleValidationFailure( + validationError: Error, + message?: string + ): Result { + return Result.fail( + UseCaseError.create( + UseCaseError.INVALID_INPUT_DATA, + message ? message : validationError.message, + validationError + ) + ); + } + + async execute(request: { + id: UniqueID; + data: IUpdateInvoice_DTO; + }): Promise { + const { id, data: invoiceDTO } = request; + + // Validaciones + const invoiceDTOOrError = ensureUpdateInvoice_DTOIsValid(invoiceDTO); + if (invoiceDTOOrError.isFailure) { + return this.handleValidationFailure(invoiceDTOOrError.error); + } + + const transaction = this._adapter.startTransaction(); + + const invoiceRepoBuilder = this.getRepository("Invoice"); + + let invoice: Invoice | null = null; + + try { + await transaction.complete(async (t) => { + invoice = await invoiceRepoBuilder({ transaction: t }).getById(id); + }); + + if (invoice === null) { + return Result.fail( + UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, `Invoice not found`, { + id: request.id.toString(), + entity: "invoice", + }) + ); + } + + return Result.ok(invoice); + } catch (error: unknown) { + const _error = error as Error; + if (invoiceRepoBuilder().isRepositoryError(_error)) { + return this.handleRepositoryError(error as BaseError, invoiceRepoBuilder()); + } else { + return this.handleUnexceptedError(error); + } + } + + // Recipient validations + /*const recipientIdOrError = ensureParticipantIdIsValid( + invoiceDTO?.recipient?.id, + ); + if (recipientIdOrError.isFailure) { + return this.handleValidationFailure( + recipientIdOrError.error, + "Recipient ID not valid", + ); + } + const recipientId = recipientIdOrError.object; + + const recipientBillingIdOrError = ensureParticipantAddressIdIsValid( + invoiceDTO?.recipient?.billing_address_id, + ); + if (recipientBillingIdOrError.isFailure) { + return this.handleValidationFailure( + recipientBillingIdOrError.error, + "Recipient billing address ID not valid", + ); + } + const recipientBillingId = recipientBillingIdOrError.object; + + const recipientShippingIdOrError = ensureParticipantAddressIdIsValid( + invoiceDTO?.recipient?.shipping_address_id, + ); + if (recipientShippingIdOrError.isFailure) { + return this.handleValidationFailure( + recipientShippingIdOrError.error, + "Recipient shipping address ID not valid", + ); + } + const recipientShippingId = recipientShippingIdOrError.object; + + const recipientContact = await this.findContact( + recipientId, + recipientBillingId, + recipientShippingId, + ); + + if (!recipientContact) { + return this.handleValidationFailure( + new Error(`Recipient with ID ${recipientId.toString()} does not exist`), + ); + } + + // Crear invoice + const invoiceOrError = await this.tryUpdateInvoiceInstance( + invoiceDTO, + invoiceIdOrError.object, + //senderId, + //senderBillingId, + //senderShippingId, + recipientContact, + ); + + if (invoiceOrError.isFailure) { + const { error: domainError } = invoiceOrError; + let errorCode = ""; + let message = ""; + + switch (domainError.code) { + case Invoice.ERROR_CUSTOMER_WITHOUT_NAME: + errorCode = UseCaseError.INVALID_INPUT_DATA; + message = + "El cliente debe ser una compañía o tener nombre y apellidos."; + break; + + default: + errorCode = UseCaseError.UNEXCEPTED_ERROR; + message = ""; + break; + } + + return Result.fail( + UseCaseError.create(errorCode, message, domainError), + ); + } + + return this.saveInvoice(invoiceOrError.object); + */ + } + + private async tryUpdateInvoiceInstance(invoiceDTO, invoiceId, recipient) { + // Create invoice + let invoice_status = InvoiceStatus.create(invoiceDTO.status).object; + if (invoice_status.isEmpty()) { + invoice_status = InvoiceStatus.createDraft(); + } + + let invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object; + if (invoice_series.isEmpty()) { + invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object; + } + + let issue_date = InvoiceDate.create(invoiceDTO.issue_date).object; + if (issue_date.isEmpty()) { + issue_date = InvoiceDate.createCurrentDate().object; + } + + let operation_date = InvoiceDate.create(invoiceDTO.operation_date).object; + if (operation_date.isEmpty()) { + operation_date = InvoiceDate.createCurrentDate().object; + } + + let invoiceCurrency = Currency.createFromCode(invoiceDTO.currency).object; + + if (invoiceCurrency.isEmpty()) { + invoiceCurrency = Currency.createDefaultCode().object; + } + + let invoiceLanguage = Language.createFromCode(invoiceDTO.language_code).object; + + if (invoiceLanguage.isEmpty()) { + invoiceLanguage = Language.createDefaultCode().object; + } + + const items = new Collection( + invoiceDTO.items?.map( + (item) => + InvoiceSimpleItem.create({ + description: Description.create(item.description).object, + quantity: Quantity.create(item.quantity).object, + unitPrice: UnitPrice.create({ + amount: item.unit_price.amount, + currencyCode: item.unit_price.currency, + precision: item.unit_price.precision, + }).object, + }).object + ) + ); + + if (!invoice_status.isDraft()) { + throw Error("Error al crear una factura que no es borrador"); + } + + return DraftInvoice.create( + { + invoiceSeries: invoice_series, + issueDate: issue_date, + operationDate: operation_date, + invoiceCurrency, + language: invoiceLanguage, + invoiceNumber: InvoiceNumber.create(undefined).object, + //notes: Note.create(invoiceDTO.notes).object, + + //senderId: UniqueID.create(null).object, + recipient, + + items, + }, + invoiceId + ); + } + + private async findContact( + contactId: UniqueID, + billingAddressId: UniqueID, + shippingAddressId: UniqueID + ) { + const contactRepoBuilder = this.getRepository("Contact"); + + const contact = await contactRepoBuilder().getById2( + contactId, + billingAddressId, + shippingAddressId + ); + + return contact; + } + + private async saveInvoice(invoice: DraftInvoice) { + const transaction = this._adapter.startTransaction(); + const invoiceRepoBuilder = this.getRepository("Invoice"); + + try { + await transaction.complete(async (t) => { + const invoiceRepo = invoiceRepoBuilder({ transaction: t }); + await invoiceRepo.save(invoice); + }); + + return Result.ok(invoice); + } catch (error: unknown) { + const _error = error as Error; + if (invoiceRepoBuilder().isRepositoryError(_error)) { + return this.handleRepositoryError(error as BaseError, invoiceRepoBuilder()); + } else { + return this.handleUnexceptedError(error); + } + } + } + + private handleUnexceptedError(error): Result { + return Result.fail( + UseCaseError.create(UseCaseError.UNEXCEPTED_ERROR, error.message, error) + ); + } + + private handleRepositoryError( + error: BaseError, + repository: IInvoiceRepository + ): Result { + const { message, details } = repository.handleRepositoryError(error); + return Result.fail( + UseCaseError.create(UseCaseError.REPOSITORY_ERROR, message, details) + ); + } +} diff --git a/modules/invoices/src/server/domain/aggregates/index.ts b/modules/invoices/src/server/domain/aggregates/index.ts new file mode 100644 index 0000000..c759eb6 --- /dev/null +++ b/modules/invoices/src/server/domain/aggregates/index.ts @@ -0,0 +1 @@ +export * from "./invoice"; diff --git a/modules/invoices/src/server/domain/aggregates/invoice.ts b/modules/invoices/src/server/domain/aggregates/invoice.ts new file mode 100644 index 0000000..c85f6d0 --- /dev/null +++ b/modules/invoices/src/server/domain/aggregates/invoice.ts @@ -0,0 +1,203 @@ +import { AggregateRoot, MoneyValue, UniqueID, UtcDate } from "@rdx/ddd-domain"; +import { Collection, Result } from "@rdx/utils"; +import { InvoiceCustomer, InvoiceItem, InvoiceItems } from "../entities"; +import { InvoiceNumber, InvoiceSerie, InvoiceStatus } from "../value-objects"; + +export interface IInvoiceProps { + invoiceNumber: InvoiceNumber; + invoiceSeries: InvoiceSerie; + + status: InvoiceStatus; + + issueDate: UtcDate; + operationDate: UtcDate; + + //dueDate: UtcDate; // ? --> depende de la forma de pago + + //tax: Tax; // ? --> detalles? + invoiceCurrency: string; + + //language: Language; + + //purchareOrderNumber: string; + //notes: Note; + + //senderId: UniqueID; + + //paymentInstructions: Note; + //paymentTerms: string; + + customer?: InvoiceCustomer; + items?: InvoiceItems; +} + +export interface IInvoice { + id: UniqueID; + invoiceNumber: InvoiceNumber; + invoiceSeries: InvoiceSerie; + + status: InvoiceStatus; + + issueDate: UtcDate; + operationDate: UtcDate; + + //senderId: UniqueID; + + customer?: InvoiceCustomer; + + //dueDate + + //tax: Tax; + //language: Language; + invoiceCurrency: string; + + //purchareOrderNumber: string; + //notes: Note; + + //paymentInstructions: Note; + //paymentTerms: string; + + items: InvoiceItems; + + calculateSubtotal: () => MoneyValue; + calculateTaxTotal: () => MoneyValue; + calculateTotal: () => MoneyValue; +} + +export class Invoice extends AggregateRoot implements IInvoice { + private _items!: Collection; + //protected _status: InvoiceStatus; + + protected constructor(props: IInvoiceProps, id?: UniqueID) { + super(props, id); + + this._items = props.items || InvoiceItems.create(); + } + + static create(props: IInvoiceProps, id?: UniqueID): Result { + const invoice = new Invoice(props, id); + + // Reglas de negocio / validaciones + // ... + // ... + + // 🔹 Disparar evento de dominio "InvoiceAuthenticatedEvent" + //const { invoice } = props; + //user.addDomainEvent(new InvoiceAuthenticatedEvent(id, invoice.toString())); + + return Result.ok(invoice); + } + + get invoiceNumber() { + return this.props.invoiceNumber; + } + + get invoiceSeries() { + return this.props.invoiceSeries; + } + + get issueDate() { + return this.props.issueDate; + } + + /*get senderId(): UniqueID { + return this.props.senderId; + }*/ + + get customer(): InvoiceCustomer | undefined { + return this.props.customer; + } + + get operationDate() { + return this.props.operationDate; + } + + /*get language() { + return this.props.language; + }*/ + + get dueDate() { + return undefined; + } + + get tax() { + return undefined; + } + + get status() { + return this.props.status; + } + + get items() { + return this._items; + } + + /*get purchareOrderNumber() { + return this.props.purchareOrderNumber; + } + + get paymentInstructions() { + return this.props.paymentInstructions; + } + + get paymentTerms() { + return this.props.paymentTerms; + } + + get billTo() { + return this.props.billTo; + } + + get shipTo() { + return this.props.shipTo; + }*/ + + get invoiceCurrency() { + return this.props.invoiceCurrency; + } + + /*get notes() { + return this.props.notes; + }*/ + + // Method to get the complete list of line items + /*get lineItems(): InvoiceLineItem[] { + return this._lineItems; + } + + addLineItem(lineItem: InvoiceLineItem, position?: number): void { + if (position === undefined) { + this._lineItems.push(lineItem); + } else { + this._lineItems.splice(position, 0, lineItem); + } + }*/ + + calculateSubtotal(): MoneyValue { + const invoiceSubtotal = MoneyValue.create({ + amount: 0, + currency_code: this.props.invoiceCurrency, + scale: 2, + }).data; + + return this._items.getAll().reduce((subtotal, item) => { + return subtotal.add(item.calculateTotal()); + }, invoiceSubtotal); + } + + // Method to calculate the total tax in the invoice + calculateTaxTotal(): MoneyValue { + const taxTotal = MoneyValue.create({ + amount: 0, + currency_code: this.props.invoiceCurrency, + scale: 2, + }).data; + + return taxTotal; + } + + // Method to calculate the total invoice amount, including taxes + calculateTotal(): MoneyValue { + return this.calculateSubtotal().add(this.calculateTaxTotal()); + } +} diff --git a/modules/invoices/src/server/domain/entities/index.ts b/modules/invoices/src/server/domain/entities/index.ts new file mode 100644 index 0000000..4c02277 --- /dev/null +++ b/modules/invoices/src/server/domain/entities/index.ts @@ -0,0 +1,2 @@ +export * from "./invoice-customer"; +export * from "./invoice-items"; diff --git a/modules/invoices/src/server/domain/entities/invoice-customer/index.ts b/modules/invoices/src/server/domain/entities/invoice-customer/index.ts new file mode 100644 index 0000000..1f5a7b2 --- /dev/null +++ b/modules/invoices/src/server/domain/entities/invoice-customer/index.ts @@ -0,0 +1,2 @@ +export * from "./invoice-address"; +export * from "./invoice-customer"; diff --git a/modules/invoices/src/server/domain/entities/invoice-customer/invoice-address.ts b/modules/invoices/src/server/domain/entities/invoice-customer/invoice-address.ts new file mode 100644 index 0000000..0cdfcca --- /dev/null +++ b/modules/invoices/src/server/domain/entities/invoice-customer/invoice-address.ts @@ -0,0 +1,78 @@ +import { EmailAddress, Name, PostalAddress, ValueObject } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { PhoneNumber } from "libphonenumber-js"; +import { InvoiceAddressType } from "../../value-objects"; + +export interface IInvoiceAddressProps { + type: InvoiceAddressType; + title: Name; + address: PostalAddress; + email: EmailAddress; + phone: PhoneNumber; +} + +export interface IInvoiceAddress { + type: InvoiceAddressType; + title: Name; + address: PostalAddress; + email: EmailAddress; + phone: PhoneNumber; +} + +export class InvoiceAddress extends ValueObject implements IInvoiceAddress { + public static create(props: IInvoiceAddressProps) { + return Result.ok(new this(props)); + } + + public static createShippingAddress(props: IInvoiceAddressProps) { + return Result.ok( + new this({ + ...props, + type: InvoiceAddressType.create("shipping").data, + }) + ); + } + + public static createBillingAddress(props: IInvoiceAddressProps) { + return Result.ok( + new this({ + ...props, + type: InvoiceAddressType.create("billing").data, + }) + ); + } + + get title(): Name { + return this.props.title; + } + + get address(): PostalAddress { + return this.props.address; + } + + get email(): EmailAddress { + return this.props.email; + } + + get phone(): PhoneNumber { + return this.props.phone; + } + + get type(): InvoiceAddressType { + return this.props.type; + } + + getValue(): IInvoiceAddressProps { + return this.props; + } + + toPrimitive() { + return { + type: this.type.toString(), + title: this.title.toString(), + address: this.address.toString(), + email: this.email.toString(), + phone: this.phone.toString(), + }; + } +} diff --git a/modules/invoices/src/server/domain/entities/invoice-customer/invoice-customer.ts b/modules/invoices/src/server/domain/entities/invoice-customer/invoice-customer.ts new file mode 100644 index 0000000..e239ff1 --- /dev/null +++ b/modules/invoices/src/server/domain/entities/invoice-customer/invoice-customer.ts @@ -0,0 +1,61 @@ +import { DomainEntity, Name, TINNumber, UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { InvoiceAddress } from "./invoice-address"; + +export interface IInvoiceCustomerProps { + tin: TINNumber; + companyName: Name; + firstName: Name; + lastName: Name; + + billingAddress?: InvoiceAddress; + shippingAddress?: InvoiceAddress; +} + +export interface IInvoiceCustomer { + id: UniqueID; + tin: TINNumber; + companyName: Name; + firstName: Name; + lastName: Name; + + billingAddress?: InvoiceAddress; + shippingAddress?: InvoiceAddress; +} + +export class InvoiceCustomer + extends DomainEntity + implements IInvoiceCustomer +{ + public static create( + props: IInvoiceCustomerProps, + id?: UniqueID + ): Result { + const participant = new InvoiceCustomer(props, id); + return Result.ok(participant); + } + + get tin(): TINNumber { + return this.props.tin; + } + + get companyName(): Name { + return this.props.companyName; + } + + get firstName(): Name { + return this.props.firstName; + } + + get lastName(): Name { + return this.props.lastName; + } + + get billingAddress() { + return this.props.billingAddress; + } + + get shippingAddress() { + return this.props.shippingAddress; + } +} diff --git a/modules/invoices/src/server/domain/entities/invoice-items/__tests__/invoice-item.test.ts b/modules/invoices/src/server/domain/entities/invoice-items/__tests__/invoice-item.test.ts new file mode 100644 index 0000000..07fc8d4 --- /dev/null +++ b/modules/invoices/src/server/domain/entities/invoice-items/__tests__/invoice-item.test.ts @@ -0,0 +1,84 @@ +import { MoneyValue, Quantity } from "@rdx/ddd-domain"; + +import { InvoiceItemDescription } from "../../../value-objects"; +import { InvoiceItem } from "../invoice-item"; + +describe("InvoiceItem", () => { + it("debería calcular correctamente el subtotal (unitPrice * quantity)", () => { + const props = { + description: InvoiceItemDescription.create("Producto A"), + quantity: Quantity.create({ amount: 200, scale: 2 }), + unitPrice: MoneyValue.create(50), + discount: Percentage.create(0), + }; + + const result = InvoiceItem.create(props); + + expect(result.isOk()).toBe(true); + const invoiceItem = result.unwrap(); + expect(invoiceItem.subtotalPrice.value).toBe(100); // 50 * 2 + }); + + it("debería calcular correctamente el total con descuento", () => { + const props = { + description: new InvoiceItemDescription("Producto B"), + quantity: new Quantity(3), + unitPrice: new MoneyValue(30), + discount: new Percentage(10), // 10% + }; + + const result = InvoiceItem.create(props); + + expect(result.isOk()).toBe(true); + const invoiceItem = result.unwrap(); + expect(invoiceItem.totalPrice.value).toBe(81); // (30 * 3) - 10% de (30 * 3) + }); + + it("debería devolver los valores correctos de las propiedades", () => { + const props = { + description: new InvoiceItemDescription("Producto C"), + quantity: new Quantity(1), + unitPrice: new MoneyValue(100), + discount: new Percentage(5), + }; + + const result = InvoiceItem.create(props); + + expect(result.isOk()).toBe(true); + const invoiceItem = result.unwrap(); + expect(invoiceItem.description.value).toBe("Producto C"); + expect(invoiceItem.quantity.value).toBe(1); + expect(invoiceItem.unitPrice.value).toBe(100); + expect(invoiceItem.discount.value).toBe(5); + }); + + it("debería manejar correctamente un descuento del 0%", () => { + const props = { + description: new InvoiceItemDescription("Producto D"), + quantity: new Quantity(4), + unitPrice: new MoneyValue(25), + discount: new Percentage(0), + }; + + const result = InvoiceItem.create(props); + + expect(result.isOk()).toBe(true); + const invoiceItem = result.unwrap(); + expect(invoiceItem.totalPrice.value).toBe(100); // 25 * 4 + }); + + it("debería manejar correctamente un descuento del 100%", () => { + const props = { + description: new InvoiceItemDescription("Producto E"), + quantity: new Quantity(2), + unitPrice: new MoneyValue(50), + discount: new Percentage(100), + }; + + const result = InvoiceItem.create(props); + + expect(result.isOk()).toBe(true); + const invoiceItem = result.unwrap(); + expect(invoiceItem.totalPrice.value).toBe(0); // (50 * 2) - 100% de (50 * 2) + }); +}); diff --git a/modules/invoices/src/server/domain/entities/invoice-items/index.ts b/modules/invoices/src/server/domain/entities/invoice-items/index.ts new file mode 100644 index 0000000..f95bd88 --- /dev/null +++ b/modules/invoices/src/server/domain/entities/invoice-items/index.ts @@ -0,0 +1,2 @@ +export * from "./invoice-item"; +export * from "./invoice-items"; diff --git a/modules/invoices/src/server/domain/entities/invoice-items/invoice-item.ts b/modules/invoices/src/server/domain/entities/invoice-items/invoice-item.ts new file mode 100644 index 0000000..9724747 --- /dev/null +++ b/modules/invoices/src/server/domain/entities/invoice-items/invoice-item.ts @@ -0,0 +1,94 @@ +import { DomainEntity, MoneyValue, Percentage, Quantity, UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { InvoiceItemDescription } from "../../value-objects"; + +export interface IInvoiceItemProps { + description: InvoiceItemDescription; + quantity: Quantity; // Cantidad de unidades + unitPrice: MoneyValue; // Precio unitario en la moneda de la factura + //subtotalPrice?: MoneyValue; // Precio unitario * Cantidad + discount: Percentage; // % descuento + //totalPrice?: MoneyValue; +} + +export interface IInvoiceItem { + id: UniqueID; + description: InvoiceItemDescription; + quantity: Quantity; + unitPrice: MoneyValue; + subtotalPrice: MoneyValue; + discount: Percentage; + totalPrice: MoneyValue; +} + +export class InvoiceItem extends DomainEntity implements IInvoiceItem { + private _subtotalPrice!: MoneyValue; + private _totalPrice!: MoneyValue; + + public static create(props: IInvoiceItemProps, id?: UniqueID): Result { + const item = new InvoiceItem(props, id); + + // Reglas de negocio / validaciones + // ... + // ... + + // 🔹 Disparar evento de dominio "InvoiceItemCreatedEvent" + //const { invoice } = props; + //user.addDomainEvent(new InvoiceAuthenticatedEvent(id, invoice.toString())); + + return Result.ok(item); + } + + get description(): InvoiceItemDescription { + return this.props.description; + } + + get quantity(): Quantity { + return this.props.quantity; + } + + get unitPrice(): MoneyValue { + return this.props.unitPrice; + } + + get subtotalPrice(): MoneyValue { + if (!this._subtotalPrice) { + this._subtotalPrice = this.calculateSubtotal(); + } + return this._subtotalPrice; + } + + get discount(): Percentage { + return this.props.discount; + } + + get totalPrice(): MoneyValue { + if (!this._totalPrice) { + this._totalPrice = this.calculateTotal(); + } + return this._totalPrice; + } + + getValue() { + return this.props; + } + + /*toPrimitive() { + return { + description: this.description.toPrimitive(), + quantity: this.quantity.toPrimitive(), + unit_price: this.unitPrice.toPrimitive(), + subtotal_price: this.subtotalPrice.toPrimitive(), + discount: this.discount.toPrimitive(), + total_price: this.totalPrice.toPrimitive(), + }; + }*/ + + calculateSubtotal(): MoneyValue { + return this.unitPrice.multiply(this.quantity.toNumber()); // Precio unitario * Cantidad + } + + calculateTotal(): MoneyValue { + return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber())); + } +} diff --git a/modules/invoices/src/server/domain/entities/invoice-items/invoice-items.ts b/modules/invoices/src/server/domain/entities/invoice-items/invoice-items.ts new file mode 100644 index 0000000..caa43f7 --- /dev/null +++ b/modules/invoices/src/server/domain/entities/invoice-items/invoice-items.ts @@ -0,0 +1,8 @@ +import { Collection } from "@rdx/utils"; +import { InvoiceItem } from "./invoice-item"; + +export class InvoiceItems extends Collection { + public static create(items?: InvoiceItem[]): InvoiceItems { + return new InvoiceItems(items); + } +} diff --git a/modules/invoices/src/server/domain/index.ts b/modules/invoices/src/server/domain/index.ts new file mode 100644 index 0000000..2c5c423 --- /dev/null +++ b/modules/invoices/src/server/domain/index.ts @@ -0,0 +1,5 @@ +export * from "./aggregates"; +export * from "./entities"; +export * from "./repositories"; +export * from "./services"; +export * from "./value-objects"; diff --git a/modules/invoices/src/server/domain/repositories/index.ts b/modules/invoices/src/server/domain/repositories/index.ts new file mode 100644 index 0000000..7a8b94a --- /dev/null +++ b/modules/invoices/src/server/domain/repositories/index.ts @@ -0,0 +1 @@ +export * from "./invoice-repository.interface"; diff --git a/modules/invoices/src/server/domain/repositories/invoice-repository.interface.ts b/modules/invoices/src/server/domain/repositories/invoice-repository.interface.ts new file mode 100644 index 0000000..203d398 --- /dev/null +++ b/modules/invoices/src/server/domain/repositories/invoice-repository.interface.ts @@ -0,0 +1,12 @@ +import { UniqueID } from "@rdx/ddd-domain"; +import { Collection, Result } from "@rdx/utils"; +import { Invoice } from "../aggregates"; + +export interface IInvoiceRepository { + findAll(transaction?: any): Promise, Error>>; + getById(id: UniqueID, transaction?: any): Promise>; + deleteById(id: UniqueID, transaction?: any): Promise>; + + create(invoice: Invoice, transaction?: any): Promise; + update(invoice: Invoice, transaction?: any): Promise; +} diff --git a/modules/invoices/src/server/domain/services/index.ts b/modules/invoices/src/server/domain/services/index.ts new file mode 100644 index 0000000..73a5d9b --- /dev/null +++ b/modules/invoices/src/server/domain/services/index.ts @@ -0,0 +1,2 @@ +export * from "./invoice-service.interface"; +export * from "./invoice.service"; diff --git a/modules/invoices/src/server/domain/services/invoice-service.interface.ts b/modules/invoices/src/server/domain/services/invoice-service.interface.ts new file mode 100644 index 0000000..d0a3266 --- /dev/null +++ b/modules/invoices/src/server/domain/services/invoice-service.interface.ts @@ -0,0 +1,22 @@ +import { UniqueID } from "@rdx/ddd-domain"; +import { Collection, Result } from "@rdx/utils"; +import { IInvoiceProps, Invoice } from "../aggregates"; + +export interface IInvoiceService { + findInvoices(transaction?: any): Promise, Error>>; + findInvoiceById(invoiceId: UniqueID, transaction?: any): Promise>; + + updateInvoiceById( + invoiceId: UniqueID, + data: Partial, + transaction?: any + ): Promise>; + + createInvoice( + invoiceId: UniqueID, + data: IInvoiceProps, + transaction?: any + ): Promise>; + + deleteInvoiceById(invoiceId: UniqueID, transaction?: any): Promise>; +} diff --git a/modules/invoices/src/server/domain/services/invoice.service.ts b/modules/invoices/src/server/domain/services/invoice.service.ts new file mode 100644 index 0000000..e8e097e --- /dev/null +++ b/modules/invoices/src/server/domain/services/invoice.service.ts @@ -0,0 +1,82 @@ +import { UniqueID } from "@rdx/ddd-domain"; +import { Collection, Result } from "@rdx/utils"; +import { Transaction } from "sequelize"; +import { IInvoiceProps, Invoice } from "../aggregates"; +import { IInvoiceRepository } from "../repositories"; +import { IInvoiceService } from "./invoice-service.interface"; + +export class InvoiceService implements IInvoiceService { + constructor(private readonly repo: IInvoiceRepository) {} + + async findInvoices(transaction?: Transaction): Promise, Error>> { + const invoicesOrError = await this.repo.findAll(transaction); + if (invoicesOrError.isFailure) { + return Result.fail(invoicesOrError.error); + } + + // Solo devolver usuarios activos + //const allInvoices = invoicesOrError.data.filter((invoice) => invoice.isActive); + //return Result.ok(new Collection(allInvoices)); + + return invoicesOrError; + } + + async findInvoiceById(invoiceId: UniqueID, transaction?: Transaction): Promise> { + return await this.repo.getById(invoiceId, transaction); + } + + async updateInvoiceById( + invoiceId: UniqueID, + data: Partial, + transaction?: Transaction + ): Promise> { + // Verificar si la factura existe + return Result.fail(new Error("No implementado")); + + const invoiceOrError = await this.repo.getById(invoiceId, transaction); + if (invoiceOrError.isFailure) { + return Result.fail(new Error("Invoice not found")); + } + + /*const updatedInvoiceOrError = Invoice.update(invoiceOrError.data, data); + if (updatedInvoiceOrError.isFailure) { + return Result.fail( + new Error(`Error updating invoice: ${updatedInvoiceOrError.error.message}`) + ); + } + + const updateInvoice = updatedInvoiceOrError.data; + + await this.repo.update(updateInvoice, transaction); + return Result.ok(updateInvoice);*/ + } + + async createInvoice( + invoiceId: UniqueID, + data: IInvoiceProps, + transaction?: Transaction + ): Promise> { + // Verificar si la factura existe + const invoiceOrError = await this.repo.getById(invoiceId, transaction); + if (invoiceOrError.isSuccess) { + return Result.fail(new Error("Invoice exists")); + } + + const newInvoiceOrError = Invoice.create(data, invoiceId); + if (newInvoiceOrError.isFailure) { + return Result.fail(new Error(`Error creating invoice: ${newInvoiceOrError.error.message}`)); + } + + const newInvoice = newInvoiceOrError.data; + + await this.repo.create(newInvoice, transaction); + return Result.ok(newInvoice); + } + + async deleteInvoiceById( + invoiceId: UniqueID, + transaction?: Transaction + ): Promise> { + return this.repo.deleteById(invoiceId, transaction); + } +} diff --git a/modules/invoices/src/server/domain/value-objects/index.ts b/modules/invoices/src/server/domain/value-objects/index.ts new file mode 100644 index 0000000..4169e9d --- /dev/null +++ b/modules/invoices/src/server/domain/value-objects/index.ts @@ -0,0 +1,5 @@ +export * from "./invoice-address-type"; +export * from "./invoice-item-description"; +export * from "./invoice-number"; +export * from "./invoice-serie"; +export * from "./invoice-status"; diff --git a/modules/invoices/src/server/domain/value-objects/invoice-address-type.ts b/modules/invoices/src/server/domain/value-objects/invoice-address-type.ts new file mode 100644 index 0000000..9f0f344 --- /dev/null +++ b/modules/invoices/src/server/domain/value-objects/invoice-address-type.ts @@ -0,0 +1,38 @@ +import { ValueObject } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; + +interface IInvoiceAddressTypeProps { + value: string; +} + +export enum INVOICE_ADDRESS_TYPE { + SHIPPING = "shipping", + BILLING = "billing", +} + +export class InvoiceAddressType extends ValueObject { + private static readonly ALLOWED_TYPES = ["shipping", "billing"]; + + static create(value: string): Result { + if (!this.ALLOWED_TYPES.includes(value)) { + return Result.fail( + new Error( + `Invalid address type: ${value}. Allowed types are: ${this.ALLOWED_TYPES.join(", ")}` + ) + ); + } + return Result.ok(new InvoiceAddressType({ value })); + } + + getValue(): string { + return this.props.value; + } + + toString(): string { + return this.getValue(); + } + + toPrimitive(): string { + return this.getValue(); + } +} diff --git a/modules/invoices/src/server/domain/value-objects/invoice-item-description.ts b/modules/invoices/src/server/domain/value-objects/invoice-item-description.ts new file mode 100644 index 0000000..a0fc4a9 --- /dev/null +++ b/modules/invoices/src/server/domain/value-objects/invoice-item-description.ts @@ -0,0 +1,50 @@ +import { ValueObject } from "@rdx/ddd-domain"; +import { Maybe, Result } from "@rdx/utils"; +import { z } from "zod"; + +interface IInvoiceItemDescriptionProps { + value: string; +} + +export class InvoiceItemDescription extends ValueObject { + private static readonly MAX_LENGTH = 255; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .max(InvoiceItemDescription.MAX_LENGTH, { + message: `Description must be at most ${InvoiceItemDescription.MAX_LENGTH} characters long`, + }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = InvoiceItemDescription.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.errors[0].message)); + } + return Result.ok(new InvoiceItemDescription({ value })); + } + + static createNullable(value?: string): Result, Error> { + if (!value || value.trim() === "") { + return Result.ok(Maybe.none()); + } + + return InvoiceItemDescription.create(value!).map((value) => Maybe.some(value)); + } + + getValue(): string { + return this.props.value; + } + + toString(): string { + return this.getValue(); + } + + toPrimitive() { + return this.getValue(); + } +} diff --git a/modules/invoices/src/server/domain/value-objects/invoice-number.ts b/modules/invoices/src/server/domain/value-objects/invoice-number.ts new file mode 100644 index 0000000..8407b29 --- /dev/null +++ b/modules/invoices/src/server/domain/value-objects/invoice-number.ts @@ -0,0 +1,42 @@ +import { ValueObject } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { z } from "zod"; + +interface IInvoiceNumberProps { + value: string; +} + +export class InvoiceNumber extends ValueObject { + private static readonly MAX_LENGTH = 255; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .max(InvoiceNumber.MAX_LENGTH, { + message: `Name must be at most ${InvoiceNumber.MAX_LENGTH} characters long`, + }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = InvoiceNumber.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.errors[0].message)); + } + return Result.ok(new InvoiceNumber({ value })); + } + + getValue(): string { + return this.props.value; + } + + toString(): string { + return this.getValue(); + } + + toPrimitive() { + return this.getValue(); + } +} diff --git a/modules/invoices/src/server/domain/value-objects/invoice-serie.ts b/modules/invoices/src/server/domain/value-objects/invoice-serie.ts new file mode 100644 index 0000000..db7176b --- /dev/null +++ b/modules/invoices/src/server/domain/value-objects/invoice-serie.ts @@ -0,0 +1,50 @@ +import { ValueObject } from "@rdx/ddd-domain"; +import { Maybe, Result } from "@rdx/utils"; +import { z } from "zod"; + +interface IInvoiceSerieProps { + value: string; +} + +export class InvoiceSerie extends ValueObject { + private static readonly MAX_LENGTH = 255; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .max(InvoiceSerie.MAX_LENGTH, { + message: `Name must be at most ${InvoiceSerie.MAX_LENGTH} characters long`, + }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = InvoiceSerie.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.errors[0].message)); + } + return Result.ok(new InvoiceSerie({ value })); + } + + static createNullable(value?: string): Result, Error> { + if (!value || value.trim() === "") { + return Result.ok(Maybe.none()); + } + + return InvoiceSerie.create(value!).map((value) => Maybe.some(value)); + } + + getValue(): string { + return this.props.value; + } + + toString(): string { + return this.getValue(); + } + + toPrimitive() { + return this.getValue(); + } +} diff --git a/modules/invoices/src/server/domain/value-objects/invoice-status.ts b/modules/invoices/src/server/domain/value-objects/invoice-status.ts new file mode 100644 index 0000000..9fc908c --- /dev/null +++ b/modules/invoices/src/server/domain/value-objects/invoice-status.ts @@ -0,0 +1,80 @@ +import { ValueObject } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; + +interface IInvoiceStatusProps { + value: string; +} + +export enum INVOICE_STATUS { + DRAFT = "draft", + EMITTED = "emitted", + SENT = "sent", + REJECTED = "rejected", +} +export class InvoiceStatus extends ValueObject { + private static readonly ALLOWED_STATUSES = ["draft", "emitted", "sent", "rejected"]; + + private static readonly TRANSITIONS: Record = { + draft: [INVOICE_STATUS.EMITTED], + emitted: [INVOICE_STATUS.SENT, INVOICE_STATUS.REJECTED, INVOICE_STATUS.DRAFT], + sent: [INVOICE_STATUS.REJECTED], + rejected: [], + }; + + static create(value: string): Result { + if (!this.ALLOWED_STATUSES.includes(value)) { + return Result.fail(new Error(`Estado de la factura no válido: ${value}`)); + } + + return Result.ok( + value === "rejected" + ? InvoiceStatus.createRejected() + : value === "sent" + ? InvoiceStatus.createSent() + : value === "emitted" + ? InvoiceStatus.createSent() + : InvoiceStatus.createDraft() + ); + } + + public static createDraft(): InvoiceStatus { + return new InvoiceStatus({ value: INVOICE_STATUS.DRAFT }); + } + + public static createEmitted(): InvoiceStatus { + return new InvoiceStatus({ value: INVOICE_STATUS.EMITTED }); + } + + public static createSent(): InvoiceStatus { + return new InvoiceStatus({ value: INVOICE_STATUS.SENT }); + } + + public static createRejected(): InvoiceStatus { + return new InvoiceStatus({ value: INVOICE_STATUS.REJECTED }); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive() { + return this.getValue(); + } + + canTransitionTo(nextStatus: string): boolean { + return InvoiceStatus.TRANSITIONS[this.props.value].includes(nextStatus); + } + + transitionTo(nextStatus: string): Result { + if (!this.canTransitionTo(nextStatus)) { + return Result.fail( + new Error(`Transición no permitida de ${this.props.value} a ${nextStatus}`) + ); + } + return InvoiceStatus.create(nextStatus); + } + + toString(): string { + return this.getValue(); + } +} diff --git a/modules/invoices/src/server/index.ts b/modules/invoices/src/server/index.ts new file mode 100644 index 0000000..ba29ee5 --- /dev/null +++ b/modules/invoices/src/server/index.ts @@ -0,0 +1,28 @@ +/* import { getService } from "@apps/server/src/core/service-registry"; */ +import { logger } from "@rdx/logger"; +import { IModuleServer } from "@rdx/modules"; +import { Application } from "express"; +import { initInvoiceModel } from "./intrastructure"; + +export const invoicesModule: IModuleServer = { + metadata: { + name: "invoices", + version: "1.0.0", + dependencies: [], + }, + init(app: Application) { + // const contacts = getService("contacts"); + //invoicesRouter(app); + logger.info("🚀 Invoices module initialized"); + }, + registerDependencies() { + logger.info("🚀 Invoices module dependencies registered"); + return { + models: [(sequelize) => initInvoiceModel(sequelize)], + services: { + getInvoice: () => {}, + /*...*/ + }, + }; + }, +}; diff --git a/modules/invoices/src/server/intrastructure/express/index.ts b/modules/invoices/src/server/intrastructure/express/index.ts new file mode 100644 index 0000000..af28f81 --- /dev/null +++ b/modules/invoices/src/server/intrastructure/express/index.ts @@ -0,0 +1 @@ +export * from "./invoices.routes"; diff --git a/modules/invoices/src/server/intrastructure/express/invoices.routes.ts b/modules/invoices/src/server/intrastructure/express/invoices.routes.ts new file mode 100644 index 0000000..65fadd5 --- /dev/null +++ b/modules/invoices/src/server/intrastructure/express/invoices.routes.ts @@ -0,0 +1,66 @@ +import { Express } from "express"; +import { + buildGetInvoiceController, + buildListInvoicesController, + ICreateInvoiceRequestSchema, +} from "../../presentation"; + +import { buildCreateInvoiceController } from "#/server/presentation/controllers/create-invoice"; +import { validateAndParseBody } from "@rdx/core"; +import { NextFunction, Request, Response, Router } from "express"; +import { Sequelize } from "sequelize"; + +export const invoicesRouter = (app: Express, database: Sequelize) => { + const routes: Router = Router({ mergeParams: true }); + + routes.get( + "/", + //checkTabContext, + //checkUser, + (req: Request, res: Response, next: NextFunction) => { + buildListInvoicesController(database).execute(req, res, next); + } + ); + + routes.get( + "/:invoiceId", + //checkTabContext, + //checkUser, + (req: Request, res: Response, next: NextFunction) => { + buildGetInvoiceController(database).execute(req, res, next); + } + ); + + routes.post( + "/", + validateAndParseBody(ICreateInvoiceRequestSchema, { sanitize: false }), + //checkTabContext, + //checkUser, + (req: Request, res: Response, next: NextFunction) => { + buildCreateInvoiceController(database).execute(req, res, next); + } + ); + + /* + routes.put( + "/:invoiceId", + validateAndParseBody(IUpdateInvoiceRequestSchema), + checkTabContext, + //checkUser, + (req: Request, res: Response, next: NextFunction) => { + buildUpdateInvoiceController().execute(req, res, next); + } + ); + + routes.delete( + "/:invoiceId", + validateAndParseBody(IDeleteInvoiceRequestSchema), + checkTabContext, + //checkUser, + (req: Request, res: Response, next: NextFunction) => { + buildDeleteInvoiceController().execute(req, res, next); + } + );*/ + + app.use("/invoices", routes); +}; diff --git a/modules/invoices/src/server/intrastructure/index.ts b/modules/invoices/src/server/intrastructure/index.ts new file mode 100644 index 0000000..f35c887 --- /dev/null +++ b/modules/invoices/src/server/intrastructure/index.ts @@ -0,0 +1,3 @@ +export * from "./express"; +export * from "./mappers"; +export * from "./sequelize"; diff --git a/modules/invoices/src/server/intrastructure/mappers/contact.mapper.ts.bak b/modules/invoices/src/server/intrastructure/mappers/contact.mapper.ts.bak new file mode 100644 index 0000000..8fdf7ee --- /dev/null +++ b/modules/invoices/src/server/intrastructure/mappers/contact.mapper.ts.bak @@ -0,0 +1,63 @@ +import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure"; +import { Name, TINNumber, UniqueID } from "@shared/contexts"; + +import { Contact, IContactProps } from "../../domain"; +import { IInvoicingContext } from "../InvoicingContext"; +import { Contact_Model, TCreationContact_Model } from "../sequelize/contact.mo.del"; +import { IContactAddressMapper, createContactAddressMapper } from "./contactAddress.mapper"; + +export interface IContactMapper + extends ISequelizeMapper {} + +class ContactMapper + extends SequelizeMapper + implements IContactMapper +{ + public constructor(props: { addressMapper: IContactAddressMapper; context: IInvoicingContext }) { + super(props); + } + + protected toDomainMappingImpl(source: Contact_Model, params: any): Contact { + if (!source.billingAddress) { + this.handleRequiredFieldError( + "billingAddress", + new Error("Missing participant's billing address") + ); + } + + if (!source.shippingAddress) { + this.handleRequiredFieldError( + "shippingAddress", + new Error("Missing participant's shipping address") + ); + } + + const billingAddress = this.props.addressMapper.mapToDomain(source.billingAddress!, params); + + const shippingAddress = this.props.addressMapper.mapToDomain(source.shippingAddress!, params); + + const props: IContactProps = { + tin: this.mapsValue(source, "tin", TINNumber.create), + firstName: this.mapsValue(source, "first_name", Name.create), + lastName: this.mapsValue(source, "last_name", Name.create), + companyName: this.mapsValue(source, "company_name", Name.create), + billingAddress, + shippingAddress, + }; + + const id = this.mapsValue(source, "id", UniqueID.create); + const contactOrError = Contact.create(props, id); + + if (contactOrError.isFailure) { + throw contactOrError.error; + } + + return contactOrError.object; + } +} + +export const createContactMapper = (context: IInvoicingContext): IContactMapper => + new ContactMapper({ + addressMapper: createContactAddressMapper(context), + context, + }); diff --git a/modules/invoices/src/server/intrastructure/mappers/contactAddress.mapper.ts.bak b/modules/invoices/src/server/intrastructure/mappers/contactAddress.mapper.ts.bak new file mode 100644 index 0000000..4e05d8f --- /dev/null +++ b/modules/invoices/src/server/intrastructure/mappers/contactAddress.mapper.ts.bak @@ -0,0 +1,65 @@ +import { + ISequelizeMapper, + SequelizeMapper, +} from "@/contexts/common/infrastructure"; +import { + City, + Country, + Email, + Note, + Phone, + PostalCode, + Province, + Street, + UniqueID, +} from "@shared/contexts"; +import { ContactAddress, IContactAddressProps } from "../../domain"; +import { IInvoicingContext } from "../InvoicingContext"; +import { + ContactAddress_Model, + TCreationContactAddress_Attributes, +} from "../sequelize"; + +export interface IContactAddressMapper + extends ISequelizeMapper< + ContactAddress_Model, + TCreationContactAddress_Attributes, + ContactAddress + > {} + +export const createContactAddressMapper = ( + context: IInvoicingContext +): IContactAddressMapper => new ContactAddressMapper({ context }); + +class ContactAddressMapper + extends SequelizeMapper< + ContactAddress_Model, + TCreationContactAddress_Attributes, + ContactAddress + > + implements IContactAddressMapper +{ + protected toDomainMappingImpl(source: ContactAddress_Model, params: any) { + const id = this.mapsValue(source, "id", UniqueID.create); + + const props: IContactAddressProps = { + type: source.type, + street: this.mapsValue(source, "street", Street.create), + city: this.mapsValue(source, "city", City.create), + province: this.mapsValue(source, "province", Province.create), + postalCode: this.mapsValue(source, "postal_code", PostalCode.create), + country: this.mapsValue(source, "country", Country.create), + email: this.mapsValue(source, "email", Email.create), + phone: this.mapsValue(source, "phone", Phone.create), + notes: this.mapsValue(source, "notes", Note.create), + }; + + const addressOrError = ContactAddress.create(props, id); + + if (addressOrError.isFailure) { + throw addressOrError.error; + } + + return addressOrError.object; + } +} diff --git a/modules/invoices/src/server/intrastructure/mappers/index.ts b/modules/invoices/src/server/intrastructure/mappers/index.ts new file mode 100644 index 0000000..722a3c8 --- /dev/null +++ b/modules/invoices/src/server/intrastructure/mappers/index.ts @@ -0,0 +1 @@ +export * from "./invoice.mapper"; diff --git a/modules/invoices/src/server/intrastructure/mappers/invoice-item.mapper.ts b/modules/invoices/src/server/intrastructure/mappers/invoice-item.mapper.ts new file mode 100644 index 0000000..9efa44e --- /dev/null +++ b/modules/invoices/src/server/intrastructure/mappers/invoice-item.mapper.ts @@ -0,0 +1,100 @@ +import { Invoice, InvoiceItem, InvoiceItemDescription } from "#/server/domain"; +import { ISequelizeMapper, MapperParamsType, SequelizeMapper } from "@rdx/core"; +import { MoneyValue, Percentage, Quantity, UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { InferCreationAttributes } from "sequelize"; +import { InvoiceItemCreationAttributes, InvoiceItemModel, InvoiceModel } from "../sequelize"; + +export interface IInvoiceItemMapper + extends ISequelizeMapper {} + +export class InvoiceItemMapper + extends SequelizeMapper + implements IInvoiceItemMapper +{ + public mapToDomain( + source: InvoiceItemModel, + params?: MapperParamsType + ): Result { + const { sourceParent } = params as { sourceParent: InvoiceModel }; + + const idOrError = UniqueID.create(source.item_id); + + const descriptionOrError = InvoiceItemDescription.create(source.description); + + const quantityOrError = Quantity.create({ + amount: source.quantity_amount, + scale: source.quantity_scale, + }); + + const unitPriceOrError = MoneyValue.create({ + amount: source.unit_price_amount, + scale: source.unit_price_scale, + currency_code: sourceParent.invoice_currency, + }); + + const discountOrError = Percentage.create({ + amount: source.discount_amount, + scale: source.discount_scale, + }); + + const result = Result.combine([ + idOrError, + descriptionOrError, + quantityOrError, + unitPriceOrError, + discountOrError, + ]); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return InvoiceItem.create( + { + description: descriptionOrError.data, + quantity: quantityOrError.data, + unitPrice: unitPriceOrError.data, + discount: discountOrError.data, + }, + idOrError.data + //sourceParent + ); + } + + public mapToPersistence( + source: InvoiceItem, + params?: MapperParamsType + ): InferCreationAttributes { + const { index, sourceParent } = params as { + index: number; + sourceParent: Invoice; + }; + + const lineData = { + parent_id: undefined, + invoice_id: sourceParent.id.toPrimitive(), + item_type: "simple", + position: index, + + item_id: source.id.toPrimitive(), + description: source.description.toPrimitive(), + + quantity_amount: source.quantity.toPrimitive().amount, + quantity_scale: source.quantity.toPrimitive().scale, + + unit_price_amount: source.unitPrice.toPrimitive().amount, + unit_price_scale: source.unitPrice.toPrimitive().scale, + + subtotal_amount: source.subtotalPrice.toPrimitive().amount, + subtotal_scale: source.subtotalPrice.toPrimitive().scale, + + discount_amount: source.discount.toPrimitive().amount, + discount_scale: source.discount.toPrimitive().scale, + + total_amount: source.totalPrice.toPrimitive().amount, + total_scale: source.totalPrice.toPrimitive().scale, + }; + return lineData; + } +} diff --git a/modules/invoices/src/server/intrastructure/mappers/invoice.mapper.ts b/modules/invoices/src/server/intrastructure/mappers/invoice.mapper.ts new file mode 100644 index 0000000..0385e37 --- /dev/null +++ b/modules/invoices/src/server/intrastructure/mappers/invoice.mapper.ts @@ -0,0 +1,97 @@ +import { Invoice, InvoiceNumber, InvoiceSerie, InvoiceStatus } from "#/server/domain"; +import { ISequelizeMapper, MapperParamsType, SequelizeMapper } from "@rdx/core"; +import { UniqueID, UtcDate } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { InvoiceCreationAttributes, InvoiceModel } from "../sequelize"; +import { InvoiceItemMapper } from "./invoice-item.mapper"; + +export interface IInvoiceMapper + extends ISequelizeMapper {} + +export class InvoiceMapper + extends SequelizeMapper + implements IInvoiceMapper +{ + private invoiceItemMapper: InvoiceItemMapper; + + constructor() { + super(); + this.invoiceItemMapper = new InvoiceItemMapper(); // Instanciar el mapper de items + } + + public mapToDomain(source: InvoiceModel, params?: MapperParamsType): Result { + const idOrError = UniqueID.create(source.id); + const statusOrError = InvoiceStatus.create(source.invoice_status); + const invoiceSeriesOrError = InvoiceSerie.create(source.invoice_series); + const invoiceNumberOrError = InvoiceNumber.create(source.invoice_number); + const issueDateOrError = UtcDate.create(source.issue_date); + const operationDateOrError = UtcDate.create(source.operation_date); + + const result = Result.combine([ + idOrError, + statusOrError, + invoiceSeriesOrError, + invoiceNumberOrError, + issueDateOrError, + operationDateOrError, + ]); + + if (result.isFailure) { + return Result.fail(result.error); + } + + // Mapear los items de la factura + const itemsOrErrors = this.invoiceItemMapper.mapArrayToDomain(source.items, { + sourceParent: source, + ...params, + }); + + if (itemsOrErrors.isFailure) { + return Result.fail(itemsOrErrors.error); + } + + const invoiceCurrency = source.invoice_currency || "EUR"; + + return Invoice.create( + { + status: statusOrError.data, + invoiceSeries: invoiceSeriesOrError.data, + invoiceNumber: invoiceNumberOrError.data, + issueDate: issueDateOrError.data, + operationDate: operationDateOrError.data, + invoiceCurrency, + items: itemsOrErrors.data, + }, + idOrError.data + ); + } + + public mapToPersistence(source: Invoice, params?: MapperParamsType): InvoiceCreationAttributes { + const subtotal = source.calculateSubtotal(); + const total = source.calculateTotal(); + + const items = this.invoiceItemMapper.mapCollectionToPersistence(source.items, params); + + return { + id: source.id.toString(), + invoice_status: source.status.toPrimitive(), + invoice_series: source.invoiceSeries.toPrimitive(), + invoice_number: source.invoiceNumber.toPrimitive(), + issue_date: source.issueDate.toPrimitive(), + operation_date: source.operationDate.toPrimitive(), + invoice_language: "es", + invoice_currency: source.invoiceCurrency || "EUR", + + subtotal_amount: subtotal.amount, + subtotal_scale: subtotal.scale, + + total_amount: total.amount, + total_scale: total.scale, + + items, + }; + } +} + +const invoiceMapper: InvoiceMapper = new InvoiceMapper(); +export { invoiceMapper }; diff --git a/modules/invoices/src/server/intrastructure/mappers/invoiceParticipant.mapper.ts.bak b/modules/invoices/src/server/intrastructure/mappers/invoiceParticipant.mapper.ts.bak new file mode 100644 index 0000000..aa6944f --- /dev/null +++ b/modules/invoices/src/server/intrastructure/mappers/invoiceParticipant.mapper.ts.bak @@ -0,0 +1,119 @@ +import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure"; +import { Name, TINNumber, UniqueID } from "@shared/contexts"; +import { + IInvoiceCustomerProps, + Invoice, + InvoiceCustomer, + InvoiceParticipantBillingAddress, + InvoiceParticipantShippingAddress, +} from "../../domain"; +import { IInvoicingContext } from "../InvoicingContext"; +import { InvoiceParticipant_Model, TCreationInvoiceParticipant_Model } from "../sequelize"; +import { + IInvoiceParticipantAddressMapper, + createInvoiceParticipantAddressMapper, +} from "./invoiceParticipantAddress.mapper"; + +export interface IInvoiceParticipantMapper + extends ISequelizeMapper< + InvoiceParticipant_Model, + TCreationInvoiceParticipant_Model, + InvoiceCustomer + > {} + +export const createInvoiceParticipantMapper = ( + context: IInvoicingContext +): IInvoiceParticipantMapper => + new InvoiceParticipantMapper({ + context, + addressMapper: createInvoiceParticipantAddressMapper(context), + }); + +class InvoiceParticipantMapper + extends SequelizeMapper< + InvoiceParticipant_Model, + TCreationInvoiceParticipant_Model, + InvoiceCustomer + > + implements IInvoiceParticipantMapper +{ + public constructor(props: { + addressMapper: IInvoiceParticipantAddressMapper; + context: IInvoicingContext; + }) { + super(props); + } + + protected toDomainMappingImpl(source: InvoiceParticipant_Model, params: any) { + /*if (!source.billingAddress) { + this.handleRequiredFieldError( + "billingAddress", + new Error("Missing participant's billing address"), + ); + } + + if (!source.shippingAddress) { + this.handleRequiredFieldError( + "shippingAddress", + new Error("Missing participant's shipping address"), + ); + } +*/ + const billingAddress = source.billingAddress + ? ((this.props.addressMapper as IInvoiceParticipantAddressMapper).mapToDomain( + source.billingAddress, + params + ) as InvoiceParticipantBillingAddress) + : undefined; + + const shippingAddress = source.shippingAddress + ? ((this.props.addressMapper as IInvoiceParticipantAddressMapper).mapToDomain( + source.shippingAddress, + params + ) as InvoiceParticipantShippingAddress) + : undefined; + + const props: IInvoiceCustomerProps = { + tin: this.mapsValue(source, "tin", TINNumber.create), + firstName: this.mapsValue(source, "first_name", Name.create), + lastName: this.mapsValue(source, "last_name", Name.create), + companyName: this.mapsValue(source, "company_name", Name.create), + billingAddress, + shippingAddress, + }; + + const id = this.mapsValue(source, "participant_id", UniqueID.create); + const participantOrError = InvoiceCustomer.create(props, id); + + if (participantOrError.isFailure) { + throw participantOrError.error; + } + + return participantOrError.object; + } + + protected toPersistenceMappingImpl( + source: InvoiceCustomer, + params: { sourceParent: Invoice } + ): TCreationInvoiceParticipant_Model { + const { sourceParent } = params; + + return { + invoice_id: sourceParent.id.toPrimitive(), + + participant_id: source.id.toPrimitive(), + tin: source.tin.toPrimitive(), + first_name: source.firstName.toPrimitive(), + last_name: source.lastName.toPrimitive(), + company_name: source.companyName.toPrimitive(), + + billingAddress: ( + this.props.addressMapper as IInvoiceParticipantAddressMapper + ).mapToPersistence(source.billingAddress!, { sourceParent: source }), + + shippingAddress: ( + this.props.addressMapper as IInvoiceParticipantAddressMapper + ).mapToPersistence(source.shippingAddress!, { sourceParent: source }), + }; + } +} diff --git a/modules/invoices/src/server/intrastructure/mappers/invoiceParticipantAddress.mapper.ts.bak b/modules/invoices/src/server/intrastructure/mappers/invoiceParticipantAddress.mapper.ts.bak new file mode 100644 index 0000000..1e898f6 --- /dev/null +++ b/modules/invoices/src/server/intrastructure/mappers/invoiceParticipantAddress.mapper.ts.bak @@ -0,0 +1,87 @@ +import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure"; +import { + City, + Country, + Email, + Note, + Phone, + PostalCode, + Province, + Street, + UniqueID, +} from "@shared/contexts"; +import { + IInvoiceParticipantAddressProps, + InvoiceCustomer, + InvoiceParticipantAddress, +} from "../../domain"; +import { IInvoicingContext } from "../InvoicingContext"; +import { + InvoiceParticipantAddress_Model, + TCreationInvoiceParticipantAddress_Model, +} from "../sequelize"; + +export interface IInvoiceParticipantAddressMapper + extends ISequelizeMapper< + InvoiceParticipantAddress_Model, + TCreationInvoiceParticipantAddress_Model, + InvoiceParticipantAddress + > {} + +export const createInvoiceParticipantAddressMapper = ( + context: IInvoicingContext +): IInvoiceParticipantAddressMapper => new InvoiceParticipantAddressMapper({ context }); + +class InvoiceParticipantAddressMapper + extends SequelizeMapper< + InvoiceParticipantAddress_Model, + TCreationInvoiceParticipantAddress_Model, + InvoiceParticipantAddress + > + implements IInvoiceParticipantAddressMapper +{ + protected toDomainMappingImpl(source: InvoiceParticipantAddress_Model, params: any) { + const id = this.mapsValue(source, "address_id", UniqueID.create); + + const props: IInvoiceParticipantAddressProps = { + type: source.type, + street: this.mapsValue(source, "street", Street.create), + city: this.mapsValue(source, "city", City.create), + province: this.mapsValue(source, "province", Province.create), + postalCode: this.mapsValue(source, "postal_code", PostalCode.create), + country: this.mapsValue(source, "country", Country.create), + email: this.mapsValue(source, "email", Email.create), + phone: this.mapsValue(source, "phone", Phone.create), + notes: this.mapsValue(source, "notes", Note.create), + }; + + const addressOrError = InvoiceParticipantAddress.create(props, id); + + if (addressOrError.isFailure) { + throw addressOrError.error; + } + + return addressOrError.object; + } + + protected toPersistenceMappingImpl( + source: InvoiceParticipantAddress, + params: { sourceParent: InvoiceCustomer } + ) { + const { sourceParent } = params; + + return { + address_id: source.id.toPrimitive(), + participant_id: sourceParent.id.toPrimitive(), + type: String(source.type), + title: source.title, + street: source.street.toPrimitive(), + city: source.city.toPrimitive(), + postal_code: source.postalCode.toPrimitive(), + province: source.province.toPrimitive(), + country: source.country.toPrimitive(), + email: source.email.toPrimitive(), + phone: source.phone.toPrimitive(), + }; + } +} diff --git a/modules/invoices/src/server/intrastructure/sequelize/contact.mo.del.ts.bak b/modules/invoices/src/server/intrastructure/sequelize/contact.mo.del.ts.bak new file mode 100644 index 0000000..d5b11b1 --- /dev/null +++ b/modules/invoices/src/server/intrastructure/sequelize/contact.mo.del.ts.bak @@ -0,0 +1,84 @@ +import { + CreationOptional, + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + NonAttribute, + Sequelize, +} from "sequelize"; + +import { ContactAddress_Model, TCreationContactAddress_Attributes } from "./contactAddress.mo.del"; + +export type TCreationContact_Model = InferCreationAttributes< + Contact_Model, + { omit: "shippingAddress" | "billingAddress" } +> & { + billingAddress: TCreationContactAddress_Attributes; + shippingAddress: TCreationContactAddress_Attributes; +}; + +export class Contact_Model extends Model< + InferAttributes, + InferCreationAttributes +> { + // To avoid table creation + static async sync(): Promise { + return Promise.resolve(); + } + + static associate(connection: Sequelize) { + const { Contact_Model, ContactAddress_Model } = connection.models; + + Contact_Model.hasOne(ContactAddress_Model, { + as: "shippingAddress", + foreignKey: "customer_id", + onDelete: "CASCADE", + }); + + Contact_Model.hasOne(ContactAddress_Model, { + as: "billingAddress", + foreignKey: "customer_id", + onDelete: "CASCADE", + }); + } + + declare id: string; + declare tin: CreationOptional; + declare company_name: CreationOptional; + declare first_name: CreationOptional; + declare last_name: CreationOptional; + + declare shippingAddress?: NonAttribute; + declare billingAddress?: NonAttribute; +} + +export default (sequelize: Sequelize) => { + Contact_Model.init( + { + id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + tin: { + type: new DataTypes.STRING(), + }, + company_name: { + type: new DataTypes.STRING(), + }, + first_name: { + type: new DataTypes.STRING(), + }, + last_name: { + type: new DataTypes.STRING(), + }, + }, + { + sequelize, + tableName: "customers", + timestamps: false, + } + ); + + return Contact_Model; +}; diff --git a/modules/invoices/src/server/intrastructure/sequelize/contactAddress.mo.del.ts.bak b/modules/invoices/src/server/intrastructure/sequelize/contactAddress.mo.del.ts.bak new file mode 100644 index 0000000..49d7e26 --- /dev/null +++ b/modules/invoices/src/server/intrastructure/sequelize/contactAddress.mo.del.ts.bak @@ -0,0 +1,75 @@ +import { + CreationOptional, + DataTypes, + ForeignKey, + InferAttributes, + InferCreationAttributes, + Model, + NonAttribute, + Sequelize, +} from "sequelize"; +import { Contact_Model } from "./contact.mo.del"; + +export type TCreationContactAddress_Attributes = InferCreationAttributes< + ContactAddress_Model, + { omit: "customer" } +>; + +export class ContactAddress_Model extends Model< + InferAttributes, + TCreationContactAddress_Attributes +> { + // To avoid table creation + static async sync(): Promise { + return Promise.resolve(); + } + + static associate(connection: Sequelize) { + const { Contact_Model, ContactAddress_Model } = connection.models; + + ContactAddress_Model.belongsTo(Contact_Model, { + as: "customer", + foreignKey: "customer_id", + }); + } + + declare id: string; + declare customer_id: ForeignKey; + declare type: string; + declare street: CreationOptional; + declare postal_code: CreationOptional; + declare city: CreationOptional; + declare province: CreationOptional; + declare country: CreationOptional; + declare phone: CreationOptional; + declare email: CreationOptional; + + declare customer?: NonAttribute; +} + +export default (sequelize: Sequelize) => { + ContactAddress_Model.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + }, + customer_id: new DataTypes.UUID(), + type: DataTypes.STRING(), + street: DataTypes.STRING(), + postal_code: DataTypes.STRING(), + city: DataTypes.STRING, + province: DataTypes.STRING, + country: DataTypes.STRING, + email: DataTypes.STRING, + phone: DataTypes.STRING, + }, + { + sequelize, + tableName: "customer_addresses", + timestamps: false, + } + ); + + return ContactAddress_Model; +}; diff --git a/modules/invoices/src/server/intrastructure/sequelize/index.ts b/modules/invoices/src/server/intrastructure/sequelize/index.ts new file mode 100644 index 0000000..3725f32 --- /dev/null +++ b/modules/invoices/src/server/intrastructure/sequelize/index.ts @@ -0,0 +1,11 @@ +import { IInvoiceRepository } from "../../domain"; +import { invoiceRepository } from "./invoice.repository"; + +export * from "./invoice-item.model"; +export * from "./invoice.model"; + +export * from "./invoice.repository"; + +export const createInvoiceRepository = (): IInvoiceRepository => { + return invoiceRepository; +}; diff --git a/modules/invoices/src/server/intrastructure/sequelize/invoice-item.model.ts b/modules/invoices/src/server/intrastructure/sequelize/invoice-item.model.ts new file mode 100644 index 0000000..198dc27 --- /dev/null +++ b/modules/invoices/src/server/intrastructure/sequelize/invoice-item.model.ts @@ -0,0 +1,166 @@ +import { + CreationOptional, + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + Sequelize, +} from "sequelize"; + +export type InvoiceItemCreationAttributes = InferCreationAttributes & {}; + +export class InvoiceItemModel extends Model< + InferAttributes, + InvoiceItemCreationAttributes +> { + static associate(connection: Sequelize) { + /*const { Invoice_Model, InvoiceItem_Model } = connection.models; + + InvoiceItem_Model.belongsTo(Invoice_Model, { + as: "invoice", + foreignKey: "invoice_id", + onDelete: "CASCADE", + });*/ + } + + declare item_id: string; + declare invoice_id: string; + + declare parent_id: CreationOptional; + declare position: number; + declare item_type: string; + + declare description: CreationOptional; + + declare quantity_amount: CreationOptional; + declare quantity_scale: CreationOptional; + + declare unit_price_amount: CreationOptional; + declare unit_price_scale: CreationOptional; + + declare subtotal_amount: CreationOptional; + declare subtotal_scale: CreationOptional; + + declare discount_amount: CreationOptional; + declare discount_scale: CreationOptional; + + declare total_amount: CreationOptional; + declare total_scale: CreationOptional; + + //declare invoice?: NonAttribute; +} + +export default (sequelize: Sequelize) => { + InvoiceItemModel.init( + { + item_id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + invoice_id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + parent_id: { + type: new DataTypes.UUID(), + allowNull: true, // Puede ser nulo para elementos de nivel superior + }, + position: { + type: new DataTypes.MEDIUMINT(), + autoIncrement: false, + allowNull: false, + }, + item_type: { + type: new DataTypes.STRING(), + allowNull: false, + defaultValue: "simple", + }, + description: { + type: new DataTypes.TEXT(), + allowNull: true, + }, + + quantity_amount: { + type: new DataTypes.BIGINT(), + allowNull: true, + defaultValue: null, + }, + quantity_scale: { + type: new DataTypes.SMALLINT(), + allowNull: true, + defaultValue: null, + }, + + unit_price_amount: { + type: new DataTypes.BIGINT(), + allowNull: true, + defaultValue: null, + }, + unit_price_scale: { + type: new DataTypes.SMALLINT(), + allowNull: true, + defaultValue: null, + }, + + /*tax_slug: { + type: new DataTypes.DECIMAL(3, 2), + allowNull: true, + }, + tax_rate: { + type: new DataTypes.DECIMAL(3, 2), + allowNull: true, + }, + tax_equalization: { + type: new DataTypes.DECIMAL(3, 2), + allowNull: true, + },*/ + + subtotal_amount: { + type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes + allowNull: true, + defaultValue: null, + }, + subtotal_scale: { + type: new DataTypes.SMALLINT(), + allowNull: true, + defaultValue: null, + }, + + discount_amount: { + type: new DataTypes.SMALLINT(), + allowNull: true, + defaultValue: null, + }, + discount_scale: { + type: new DataTypes.SMALLINT(), + allowNull: true, + defaultValue: null, + }, + + /*tax_amount: { + type: new DataTypes.BIGINT(), + allowNull: true, + },*/ + total_amount: { + type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes + allowNull: true, + defaultValue: null, + }, + total_scale: { + type: new DataTypes.SMALLINT(), + allowNull: true, + defaultValue: null, + }, + }, + { + sequelize, + tableName: "invoice_items", + + defaultScope: {}, + + scopes: {}, + } + ); + + return InvoiceItemModel; +}; diff --git a/modules/invoices/src/server/intrastructure/sequelize/invoice.model.ts b/modules/invoices/src/server/intrastructure/sequelize/invoice.model.ts new file mode 100644 index 0000000..75d3284 --- /dev/null +++ b/modules/invoices/src/server/intrastructure/sequelize/invoice.model.ts @@ -0,0 +1,141 @@ +import { + CreationOptional, + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + NonAttribute, + Sequelize, +} from "sequelize"; +import { InvoiceItemCreationAttributes, InvoiceItemModel } from "./invoice-item.model"; + +export type InvoiceCreationAttributes = InferCreationAttributes & { + items?: InvoiceItemCreationAttributes[]; +}; + +export class InvoiceModel extends Model, InvoiceCreationAttributes> { + static associate(connection: Sequelize) { + const { InvoiceModel, InvoiceItemModel } = connection.models; + + InvoiceModel.hasMany(InvoiceItemModel, { + as: "items", + foreignKey: "invoice_id", + onDelete: "CASCADE", + }); + } + + declare id: string; + + declare invoice_status: string; + declare invoice_series: CreationOptional; + declare invoice_number: CreationOptional; + declare issue_date: CreationOptional; + declare operation_date: CreationOptional; + declare invoice_language: string; + declare invoice_currency: string; + + // Subtotal + declare subtotal_amount: CreationOptional; + declare subtotal_scale: CreationOptional; + + // Total + declare total_amount: CreationOptional; + declare total_scale: CreationOptional; + + // Relationships + declare items: NonAttribute; + //declare customer: NonAttribute; +} + +const initInvoiceModel = (sequelize: Sequelize) => { + return sequelize.define( + "InvoiceModel", + { + id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + + invoice_status: { + type: new DataTypes.STRING(), + allowNull: false, + }, + + invoice_series: { + type: new DataTypes.STRING(), + allowNull: true, + defaultValue: null, + }, + + invoice_number: { + type: new DataTypes.STRING(), + allowNull: true, + defaultValue: null, + }, + + issue_date: { + type: new DataTypes.DATEONLY(), + allowNull: true, + defaultValue: null, + }, + + operation_date: { + type: new DataTypes.DATEONLY(), + allowNull: true, + defaultValue: null, + }, + + invoice_language: { + type: new DataTypes.STRING(), + allowNull: false, + }, + + invoice_currency: { + type: new DataTypes.STRING(3), // ISO 4217 + allowNull: false, + }, + + subtotal_amount: { + type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes + allowNull: true, + defaultValue: null, + }, + subtotal_scale: { + type: new DataTypes.SMALLINT(), + allowNull: true, + defaultValue: null, + }, + + total_amount: { + type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes + allowNull: true, + defaultValue: null, + }, + total_scale: { + type: new DataTypes.SMALLINT(), + allowNull: true, + defaultValue: null, + }, + }, + { + tableName: "invoices", + + paranoid: true, // softs deletes + timestamps: true, + + createdAt: "created_at", + updatedAt: "updated_at", + deletedAt: "deleted_at", + + indexes: [{ unique: true, fields: ["invoice_number"] }], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + + defaultScope: {}, + + scopes: {}, + } + ); +}; + +export { initInvoiceModel }; diff --git a/modules/invoices/src/server/intrastructure/sequelize/invoice.repository.ts b/modules/invoices/src/server/intrastructure/sequelize/invoice.repository.ts new file mode 100644 index 0000000..a546b53 --- /dev/null +++ b/modules/invoices/src/server/intrastructure/sequelize/invoice.repository.ts @@ -0,0 +1,111 @@ +import { SequelizeRepository } from "@rdx/core"; +import { UniqueID } from "@rdx/ddd-domain"; +import { Collection, Result } from "@rdx/utils"; +import { Transaction } from "sequelize"; +import { IInvoiceRepository, Invoice } from "../../domain"; +import { IInvoiceMapper, invoiceMapper } from "../mappers/invoice.mapper"; +import { InvoiceItemModel } from "./invoice-item.model"; +import { InvoiceModel } from "./invoice.model"; + +class InvoiceRepository extends SequelizeRepository implements IInvoiceRepository { + private readonly _mapper!: IInvoiceMapper; + + /** + * 🔹 Función personalizada para mapear errores de unicidad en autenticación + */ + private _customErrorMapper(error: Error): string | null { + if (error.name === "SequelizeUniqueConstraintError") { + return "Invoice with this email already exists"; + } + + return null; + } + + constructor(mapper: IInvoiceMapper) { + super(); + this._mapper = mapper; + } + + async invoiceExists(id: UniqueID, transaction?: Transaction): Promise> { + try { + const _invoice = await this._getById(InvoiceModel, id, {}, transaction); + + return Result.ok(Boolean(id.equals(_invoice.id))); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async findAll(transaction?: Transaction): Promise, Error>> { + try { + const rawInvoices: any = await this._findAll( + InvoiceModel, + { + include: [ + { + model: InvoiceItemModel, + as: "items", + }, + ], + }, + transaction + ); + + if (!rawInvoices === true) { + return Result.fail(new Error("Invoice with email not exists")); + } + + return this._mapper.mapArrayToDomain(rawInvoices); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async getById(id: UniqueID, transaction?: Transaction): Promise> { + try { + const rawInvoice: any = await this._getById( + InvoiceModel, + id, + { + include: [ + { + model: InvoiceItemModel, + as: "items", + }, + ], + }, + transaction + ); + + if (!rawInvoice === true) { + return Result.fail(new Error(`Invoice with id ${id.toString()} not exists`)); + } + + return this._mapper.mapToDomain(rawInvoice); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async deleteById(id: UniqueID, transaction?: Transaction): Promise> { + try { + this._deleteById(InvoiceModel, id); + return Result.ok(true); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async create(invoice: Invoice, transaction?: Transaction): Promise { + const invoiceData = this._mapper.mapToPersistence(invoice); + await this._save(InvoiceModel, invoice.id, invoiceData, {}, transaction); + } + + async update(invoice: Invoice, transaction?: Transaction): Promise { + const invoiceData = this._mapper.mapToPersistence(invoice); + await this._save(InvoiceModel, invoice.id, invoiceData, {}, transaction); + } +} + +const invoiceRepository = new InvoiceRepository(invoiceMapper); +export { invoiceRepository }; diff --git a/modules/invoices/src/server/intrastructure/sequelize/invoiceParticipant.mo.del.ts.bak b/modules/invoices/src/server/intrastructure/sequelize/invoiceParticipant.mo.del.ts.bak new file mode 100644 index 0000000..151b22c --- /dev/null +++ b/modules/invoices/src/server/intrastructure/sequelize/invoiceParticipant.mo.del.ts.bak @@ -0,0 +1,106 @@ +import { + CreationOptional, + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + NonAttribute, + Sequelize, +} from "sequelize"; +import { InvoiceModel } from "./invoice.model"; +import { + InvoiceParticipantAddress_Model, + TCreationInvoiceParticipantAddress_Model, +} from "./invoiceParticipantAddress.mo.del"; + +export type TCreationInvoiceParticipant_Model = InferCreationAttributes< + InvoiceParticipant_Model, + { omit: "shippingAddress" | "billingAddress" | "invoice" } +> & { + billingAddress: TCreationInvoiceParticipantAddress_Model; + shippingAddress: TCreationInvoiceParticipantAddress_Model; +}; + +export class InvoiceParticipant_Model extends Model< + InferAttributes< + InvoiceParticipant_Model, + { omit: "shippingAddress" | "billingAddress" | "invoice" } + >, + InferCreationAttributes< + InvoiceParticipant_Model, + { omit: "shippingAddress" | "billingAddress" | "invoice" } + > +> { + static associate(connection: Sequelize) { + const { Invoice_Model, InvoiceParticipantAddress_Model, InvoiceParticipant_Model } = + connection.models; + + InvoiceParticipant_Model.belongsTo(Invoice_Model, { + as: "invoice", + foreignKey: "invoice_id", + onDelete: "CASCADE", + }); + + InvoiceParticipant_Model.hasOne(InvoiceParticipantAddress_Model, { + as: "shippingAddress", + foreignKey: "participant_id", + onDelete: "CASCADE", + }); + + InvoiceParticipant_Model.hasOne(InvoiceParticipantAddress_Model, { + as: "billingAddress", + foreignKey: "participant_id", + onDelete: "CASCADE", + }); + } + + declare participant_id: string; + declare invoice_id: string; + declare tin: CreationOptional; + declare company_name: CreationOptional; + declare first_name: CreationOptional; + declare last_name: CreationOptional; + + declare shippingAddress?: NonAttribute; + declare billingAddress?: NonAttribute; + + declare invoice?: NonAttribute; +} + +export default (sequelize: Sequelize) => { + InvoiceParticipant_Model.init( + { + participant_id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + invoice_id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + tin: { + type: new DataTypes.STRING(), + allowNull: true, + }, + company_name: { + type: new DataTypes.STRING(), + allowNull: true, + }, + first_name: { + type: new DataTypes.STRING(), + allowNull: true, + }, + last_name: { + type: new DataTypes.STRING(), + allowNull: true, + }, + }, + { + sequelize, + tableName: "invoice_participants", + timestamps: false, + } + ); + + return InvoiceParticipant_Model; +}; diff --git a/modules/invoices/src/server/intrastructure/sequelize/invoiceParticipantAddress.mo.del.ts.bak b/modules/invoices/src/server/intrastructure/sequelize/invoiceParticipantAddress.mo.del.ts.bak new file mode 100644 index 0000000..216e348 --- /dev/null +++ b/modules/invoices/src/server/intrastructure/sequelize/invoiceParticipantAddress.mo.del.ts.bak @@ -0,0 +1,94 @@ +import { + CreationOptional, + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + NonAttribute, + Sequelize, +} from "sequelize"; +import { InvoiceParticipant_Model } from "./invoiceParticipant.mo.del"; + +export type TCreationInvoiceParticipantAddress_Model = InferCreationAttributes< + InvoiceParticipantAddress_Model, + { omit: "participant" } +>; + +export class InvoiceParticipantAddress_Model extends Model< + InferAttributes, + InferCreationAttributes +> { + static associate(connection: Sequelize) { + const { InvoiceParticipantAddress_Model, InvoiceParticipant_Model } = connection.models; + InvoiceParticipantAddress_Model.belongsTo(InvoiceParticipant_Model, { + as: "participant", + foreignKey: "participant_id", + }); + } + + declare address_id: string; + declare participant_id: string; + declare type: string; + declare street: CreationOptional; + declare postal_code: CreationOptional; + declare city: CreationOptional; + declare province: CreationOptional; + declare country: CreationOptional; + declare phone: CreationOptional; + declare email: CreationOptional; + + declare participant?: NonAttribute; +} + +export default (sequelize: Sequelize) => { + InvoiceParticipantAddress_Model.init( + { + address_id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + participant_id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + type: { + type: new DataTypes.STRING(), + allowNull: false, + }, + street: { + type: new DataTypes.STRING(), + allowNull: true, + }, + postal_code: { + type: new DataTypes.STRING(), + allowNull: true, + }, + city: { + type: new DataTypes.STRING(), + allowNull: true, + }, + province: { + type: new DataTypes.STRING(), + allowNull: true, + }, + country: { + type: new DataTypes.STRING(), + allowNull: true, + }, + email: { + type: new DataTypes.STRING(), + allowNull: true, + }, + phone: { + type: new DataTypes.STRING(), + allowNull: true, + }, + }, + { + sequelize, + tableName: "invoice_participant_addresses", + } + ); + + return InvoiceParticipantAddress_Model; +}; diff --git a/modules/invoices/src/server/presentation/controllers/create-invoice/create-invoice.controller.ts b/modules/invoices/src/server/presentation/controllers/create-invoice/create-invoice.controller.ts new file mode 100644 index 0000000..06c1324 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/create-invoice/create-invoice.controller.ts @@ -0,0 +1,45 @@ +import { ExpressController } from "@rdx/core"; +import { UniqueID } from "@rdx/ddd-domain"; +import { CreateInvoiceUseCase } from "../../../application"; +import { ICreateInvoiceRequestDTO } from "../../dto"; +import { ICreateInvoicePresenter } from "./presenter"; + +export class CreateInvoiceController extends ExpressController { + public constructor( + private readonly createInvoice: CreateInvoiceUseCase, + private readonly presenter: ICreateInvoicePresenter + ) { + super(); + } + + protected async executeImpl() { + const createDTO: ICreateInvoiceRequestDTO = this.req.body; + + // Validar ID + const invoiceIdOrError = UniqueID.create(createDTO.id); + if (invoiceIdOrError.isFailure) return this.invalidInputError("Invoice ID not valid"); + + const invoiceOrError = await this.createInvoice.execute(invoiceIdOrError.data, createDTO); + + if (invoiceOrError.isFailure) { + return this.handleError(invoiceOrError.error); + } + + return this.ok(this.presenter.toDTO(invoiceOrError.data)); + } + + private handleError(error: Error) { + const message = error.message; + + if ( + message.includes("Database connection lost") || + message.includes("Database request timed out") + ) { + return this.unavailableError( + "Database service is currently unavailable. Please try again later." + ); + } + + return this.conflictError(message); + } +} diff --git a/modules/invoices/src/server/presentation/controllers/create-invoice/index.ts b/modules/invoices/src/server/presentation/controllers/create-invoice/index.ts new file mode 100644 index 0000000..bdf1714 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/create-invoice/index.ts @@ -0,0 +1,17 @@ +import { CreateInvoiceUseCase } from "#/server/application"; +import { InvoiceService } from "#/server/domain"; +import { invoiceRepository } from "#/server/intrastructure"; +import { SequelizeTransactionManager } from "@rdx/core"; +import { Sequelize } from "sequelize"; +import { CreateInvoiceController } from "./create-invoice.controller"; +import { createInvoicePresenter } from "./presenter"; + +export const buildCreateInvoiceController = (database: Sequelize) => { + const transactionManager = new SequelizeTransactionManager(database); + const invoiceService = new InvoiceService(invoiceRepository); + + const useCase = new CreateInvoiceUseCase(invoiceService, transactionManager); + const presenter = createInvoicePresenter; + + return new CreateInvoiceController(useCase, presenter); +}; diff --git a/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/InvoiceItem.presenter.ts.bak b/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/InvoiceItem.presenter.ts.bak new file mode 100644 index 0000000..3177298 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/InvoiceItem.presenter.ts.bak @@ -0,0 +1,19 @@ +import { InvoiceItem } from "@/contexts/invoicing/domain/InvoiceItems"; +import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; +import { ICollection, IMoney_Response_DTO } from "@shared/contexts"; + +export const invoiceItemPresenter = ( + items: ICollection, + context: IInvoicingContext +) => + items.totalCount > 0 + ? items.items.map((item: InvoiceItem) => ({ + description: item.description.toString(), + quantity: item.quantity.toString(), + unit_measure: "", + unit_price: item.unitPrice.toPrimitive() as IMoney_Response_DTO, + subtotal: item.calculateSubtotal().toPrimitive() as IMoney_Response_DTO, + tax_amount: item.calculateTaxAmount().toPrimitive() as IMoney_Response_DTO, + total: item.calculateTotal().toPrimitive() as IMoney_Response_DTO, + })) + : []; diff --git a/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/InvoiceParticipant.presenter.ts.bak b/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/InvoiceParticipant.presenter.ts.bak new file mode 100644 index 0000000..ea62800 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/InvoiceParticipant.presenter.ts.bak @@ -0,0 +1,26 @@ +import { IInvoiceParticipant } from "@/contexts/invoicing/domain"; +import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; +import { ICreateInvoice_Participant_Response_DTO } from "@shared/contexts"; +import { InvoiceParticipantAddressPresenter } from "./InvoiceParticipantAddress.presenter"; + +export const InvoiceParticipantPresenter = ( + participant: IInvoiceParticipant, + context: IInvoicingContext, +): ICreateInvoice_Participant_Response_DTO | undefined => { + return { + id: participant.id.toString(), + tin: participant.tin.toString(), + first_name: participant.firstName.toString(), + last_name: participant.lastName.toString(), + company_name: participant.companyName.toString(), + + billing_address: InvoiceParticipantAddressPresenter( + participant.billingAddress!, + context, + ), + shipping_address: InvoiceParticipantAddressPresenter( + participant.shippingAddress!, + context, + ), + }; +}; diff --git a/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak b/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak new file mode 100644 index 0000000..4e63728 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak @@ -0,0 +1,19 @@ +import { InvoiceParticipantAddress } from "@/contexts/invoicing/domain"; +import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; +import { ICreateInvoice_AddressParticipant_Response_DTO } from "@shared/contexts"; + +export const InvoiceParticipantAddressPresenter = ( + address: InvoiceParticipantAddress, + context: IInvoicingContext, +): ICreateInvoice_AddressParticipant_Response_DTO => { + return { + id: address.id.toString(), + street: address.street.toString(), + city: address.city.toString(), + postal_code: address.postalCode.toString(), + province: address.province.toString(), + country: address.country.toString(), + email: address.email.toString(), + phone: address.phone.toString(), + }; +}; diff --git a/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/create-invoice.presenter.ts b/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/create-invoice.presenter.ts new file mode 100644 index 0000000..1ba9b0a --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/create-invoice.presenter.ts @@ -0,0 +1,28 @@ +import { Invoice } from "#/server/domain"; +import { ICreateInvoiceResponseDTO } from "#/server/presentation/dto"; + +export interface ICreateInvoicePresenter { + toDTO: (invoice: Invoice) => ICreateInvoiceResponseDTO; +} + +export const createInvoicePresenter: ICreateInvoicePresenter = { + toDTO: (invoice: Invoice): ICreateInvoiceResponseDTO => ({ + id: invoice.id.toString(), + + invoice_status: invoice.status.toString(), + invoice_number: invoice.invoiceNumber.toString(), + invoice_series: invoice.invoiceSeries.toString(), + issue_date: invoice.issueDate.toDateString(), + operation_date: invoice.operationDate.toDateString(), + language_code: "es", + currency: invoice.invoiceCurrency || "EUR", + subtotal: invoice.calculateSubtotal().toPrimitive(), + total: invoice.calculateTotal().toPrimitive(), + + //sender: {}, //await InvoiceParticipantPresenter(invoice.senderId, context), + + //customer: InvoiceParticipantPresenter(invoice.recipient, context), + + //items: invoiceItemPresenter(invoice.items, context), + }), +}; diff --git a/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/index.ts b/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/index.ts new file mode 100644 index 0000000..3677365 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/create-invoice/presenter/index.ts @@ -0,0 +1 @@ +export * from "./create-invoice.presenter"; diff --git a/modules/invoices/src/server/presentation/controllers/delete-invoice/delete-invoice.controller.ts.bak b/modules/invoices/src/server/presentation/controllers/delete-invoice/delete-invoice.controller.ts.bak new file mode 100644 index 0000000..b1c05d9 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/delete-invoice/delete-invoice.controller.ts.bak @@ -0,0 +1,12 @@ +import { DeleteInvoiceUseCase } from "@contexts/invoices/application"; +import { ExpressController } from "core/common/presentation"; + +export class DeleteInvoiceController extends ExpressController { + public constructor(private readonly deleteInvoice: DeleteInvoiceUseCase) { + super(); + } + + async executeImpl(): Promise { + return this.noContent(); + } +} diff --git a/modules/invoices/src/server/presentation/controllers/delete-invoice/index.ts.bak b/modules/invoices/src/server/presentation/controllers/delete-invoice/index.ts.bak new file mode 100644 index 0000000..3e31d06 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/delete-invoice/index.ts.bak @@ -0,0 +1,14 @@ +import { DeleteInvoiceUseCase } from "@contexts/invoices/application"; +import { InvoiceService } from "@contexts/invoices/domain"; +import { invoiceRepository } from "@contexts/invoices/intrastructure"; +import { SequelizeTransactionManager } from "core/common/infrastructure"; +import { DeleteInvoiceController } from "./delete-invoice.controller"; + +export const buildDeleteInvoiceController = () => { + const transactionManager = new SequelizeTransactionManager(); + const invoiceService = new InvoiceService(invoiceRepository); + + const useCase = new DeleteInvoiceUseCase(invoiceService, transactionManager); + + return new DeleteInvoiceController(useCase); +}; diff --git a/modules/invoices/src/server/presentation/controllers/get-invoice/get-invoice.controller.ts b/modules/invoices/src/server/presentation/controllers/get-invoice/get-invoice.controller.ts new file mode 100644 index 0000000..624d366 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/get-invoice/get-invoice.controller.ts @@ -0,0 +1,44 @@ +import { GetInvoiceUseCase } from "#/server/application"; +import { ExpressController } from "@rdx/core"; +import { UniqueID } from "@rdx/ddd-domain"; +import { IGetInvoicePresenter } from "./presenter"; + +export class GetInvoiceController extends ExpressController { + public constructor( + private readonly getInvoice: GetInvoiceUseCase, + private readonly presenter: IGetInvoicePresenter + ) { + super(); + } + + protected async executeImpl() { + const { invoiceId } = this.req.params; + + // Validar ID + const invoiceIdOrError = UniqueID.create(invoiceId); + if (invoiceIdOrError.isFailure) return this.invalidInputError("Invoice ID not valid"); + + const invoiceOrError = await this.getInvoice.execute(invoiceIdOrError.data); + + if (invoiceOrError.isFailure) { + return this.handleError(invoiceOrError.error); + } + + return this.ok(this.presenter.toDTO(invoiceOrError.data)); + } + + private handleError(error: Error) { + const message = error.message; + + if ( + message.includes("Database connection lost") || + message.includes("Database request timed out") + ) { + return this.unavailableError( + "Database service is currently unavailable. Please try again later." + ); + } + + return this.conflictError(message); + } +} diff --git a/modules/invoices/src/server/presentation/controllers/get-invoice/index.ts b/modules/invoices/src/server/presentation/controllers/get-invoice/index.ts new file mode 100644 index 0000000..6342a77 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/get-invoice/index.ts @@ -0,0 +1,17 @@ +import { GetInvoiceUseCase } from "#/server/application"; +import { InvoiceService } from "#/server/domain"; +import { invoiceRepository } from "#/server/intrastructure"; +import { SequelizeTransactionManager } from "@rdx/core"; +import { Sequelize } from "sequelize"; +import { GetInvoiceController } from "./get-invoice.controller"; +import { getInvoicePresenter } from "./presenter"; + +export const buildGetInvoiceController = (database: Sequelize) => { + const transactionManager = new SequelizeTransactionManager(database); + const invoiceService = new InvoiceService(invoiceRepository); + + const useCase = new GetInvoiceUseCase(invoiceService, transactionManager); + const presenter = getInvoicePresenter; + + return new GetInvoiceController(useCase, presenter); +}; diff --git a/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/InvoiceItem.presenter.ts.bak b/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/InvoiceItem.presenter.ts.bak new file mode 100644 index 0000000..8599c3d --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/InvoiceItem.presenter.ts.bak @@ -0,0 +1,16 @@ +import { InvoiceItem } from "#/server/domain"; +import { IInvoicingContext } from "#/server/intrastructure"; +import { Collection } from "@rdx/utils"; + +export const invoiceItemPresenter = (items: Collection, context: IInvoicingContext) => + items.totalCount > 0 + ? items.items.map((item: InvoiceItem) => ({ + description: item.description.toString(), + quantity: item.quantity.toString(), + unit_measure: "", + unit_price: item.unitPrice.toPrimitive() as IMoney_Response_DTO, + subtotal: item.calculateSubtotal().toPrimitive() as IMoney_Response_DTO, + tax_amount: item.calculateTaxAmount().toPrimitive() as IMoney_Response_DTO, + total: item.calculateTotal().toPrimitive() as IMoney_Response_DTO, + })) + : []; diff --git a/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/InvoiceParticipant.presenter.ts.bak b/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/InvoiceParticipant.presenter.ts.bak new file mode 100644 index 0000000..635aa69 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/InvoiceParticipant.presenter.ts.bak @@ -0,0 +1,26 @@ +import { IInvoiceParticipant } from "@/contexts/invoicing/domain"; +import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; +import { ICreateInvoice_Participant_Response_DTO } from "@shared/contexts"; +import { InvoiceParticipantAddressPresenter } from "./InvoiceParticipantAddress.presenter"; + +export const InvoiceParticipantPresenter = async ( + participant: IInvoiceParticipant, + context: IInvoicingContext, +): Promise => { + return { + id: participant.id.toString(), + tin: participant.tin.toString(), + first_name: participant.firstName.toString(), + last_name: participant.lastName.toString(), + company_name: participant.companyName.toString(), + + billing_address: await InvoiceParticipantAddressPresenter( + participant.billingAddress!, + context, + ), + shipping_address: await InvoiceParticipantAddressPresenter( + participant.shippingAddress!, + context, + ), + }; +}; diff --git a/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak b/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak new file mode 100644 index 0000000..15478c1 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak @@ -0,0 +1,19 @@ +import { InvoiceParticipantAddress } from "@/contexts/invoicing/domain"; +import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; +import { ICreateInvoice_AddressParticipant_Response_DTO } from "@shared/contexts"; + +export const InvoiceParticipantAddressPresenter = async ( + address: InvoiceParticipantAddress, + context: IInvoicingContext, +): Promise => { + return { + id: address.id.toString(), + street: address.street.toString(), + city: address.city.toString(), + postal_code: address.postalCode.toString(), + province: address.province.toString(), + country: address.country.toString(), + email: address.email.toString(), + phone: address.phone.toString(), + }; +}; diff --git a/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/get-invoice.presenter.ts b/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/get-invoice.presenter.ts new file mode 100644 index 0000000..1496f6c --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/get-invoice.presenter.ts @@ -0,0 +1,59 @@ +import { Invoice, InvoiceItem } from "#/server/domain"; +import { IGetInvoiceResponseDTO } from "../../../dto"; + +export interface IGetInvoicePresenter { + toDTO: (invoice: Invoice) => IGetInvoiceResponseDTO; +} + +export const getInvoicePresenter: IGetInvoicePresenter = { + toDTO: (invoice: Invoice): IGetInvoiceResponseDTO => ({ + id: invoice.id.toPrimitive(), + + invoice_status: invoice.status.toString(), + invoice_number: invoice.invoiceNumber.toString(), + invoice_series: invoice.invoiceSeries.toString(), + issue_date: invoice.issueDate.toDateString(), + operation_date: invoice.operationDate.toDateString(), + language_code: "ES", + currency: invoice.invoiceCurrency.toString(), + subtotal: invoice.calculateSubtotal().toPrimitive(), + total: invoice.calculateTotal().toPrimitive(), + + items: + invoice.items.size() > 0 + ? invoice.items.map((item: InvoiceItem) => ({ + description: item.description.toString(), + quantity: item.quantity.toPrimitive(), + unit_measure: "", + unit_price: item.unitPrice.toPrimitive(), + subtotal: item.calculateSubtotal().toPrimitive(), + //tax_amount: item.calculateTaxAmount().toPrimitive(), + total: item.calculateTotal().toPrimitive(), + })) + : [], + + //sender: {}, //await InvoiceParticipantPresenter(invoice.senderId, context), + + /*recipient: await InvoiceParticipantPresenter(invoice.recipient, context), + items: invoiceItemPresenter(invoice.items, context), + + payment_term: { + payment_type: "", + due_date: "", + }, + + due_amount: { + currency: invoice.currency.toString(), + precision: 2, + amount: 0, + }, + + custom_fields: [], + + metadata: { + create_time: "", + last_updated_time: "", + delete_time: "", + },*/ + }), +}; diff --git a/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/index.ts b/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/index.ts new file mode 100644 index 0000000..60624c1 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/get-invoice/presenter/index.ts @@ -0,0 +1 @@ +export * from "./get-invoice.presenter"; diff --git a/modules/invoices/src/server/presentation/controllers/index.ts b/modules/invoices/src/server/presentation/controllers/index.ts new file mode 100644 index 0000000..52d26ae --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/index.ts @@ -0,0 +1,5 @@ +//export * from "./create-invoice"; +//export * from "./delete-invoice"; +export * from "./get-invoice"; +export * from "./list-invoices"; +///export * from "./update-invoice"; diff --git a/modules/invoices/src/server/presentation/controllers/list-invoices/index.ts b/modules/invoices/src/server/presentation/controllers/list-invoices/index.ts new file mode 100644 index 0000000..862dce9 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/list-invoices/index.ts @@ -0,0 +1,17 @@ +import { ListInvoicesUseCase } from "#/server/application"; +import { InvoiceService } from "#/server/domain"; +import { invoiceRepository } from "#/server/intrastructure"; +import { SequelizeTransactionManager } from "@rdx/core"; +import { Sequelize } from "sequelize"; +import { ListInvoicesController } from "./list-invoices.controller"; +import { listInvoicesPresenter } from "./presenter"; + +export const buildListInvoicesController = (database: Sequelize) => { + const transactionManager = new SequelizeTransactionManager(database); + const invoiceService = new InvoiceService(invoiceRepository); + + const useCase = new ListInvoicesUseCase(invoiceService, transactionManager); + const presenter = listInvoicesPresenter; + + return new ListInvoicesController(useCase, presenter); +}; diff --git a/modules/invoices/src/server/presentation/controllers/list-invoices/list-invoices.controller.ts b/modules/invoices/src/server/presentation/controllers/list-invoices/list-invoices.controller.ts new file mode 100644 index 0000000..48a88eb --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/list-invoices/list-invoices.controller.ts @@ -0,0 +1,47 @@ +import { ListInvoicesUseCase } from "#/server/application"; +import { ExpressController } from "@rdx/core"; +import { IListInvoicesPresenter } from "./presenter"; + +export class ListInvoicesController extends ExpressController { + public constructor( + private readonly listInvoices: ListInvoicesUseCase, + private readonly presenter: IListInvoicesPresenter + ) { + super(); + } + + protected async executeImpl() { + const { query } = this.req; + //const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(query); + + const invoicesOrError = await this.listInvoices.execute(/* queryCriteria */); + + if (invoicesOrError.isFailure) { + return this.handleError(invoicesOrError.error); + } + + return this.ok( + this.presenter.toDTO( + invoicesOrError.data /*, { + page: queryCriteria.pagination.offset, + limit: queryCriteria.pagination.limit, + }*/ + ) + ); + } + + private handleError(error: Error) { + const message = error.message; + + if ( + message.includes("Database connection lost") || + message.includes("Database request timed out") + ) { + return this.unavailableError( + "Database service is currently unavailable. Please try again later." + ); + } + + return this.conflictError(message); + } +} diff --git a/modules/invoices/src/server/presentation/controllers/list-invoices/presenter/InvoiceParticipant.presenter.ts.bak b/modules/invoices/src/server/presentation/controllers/list-invoices/presenter/InvoiceParticipant.presenter.ts.bak new file mode 100644 index 0000000..a6030b1 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/list-invoices/presenter/InvoiceParticipant.presenter.ts.bak @@ -0,0 +1,22 @@ +import { IInvoiceParticipant } from "@/contexts/invoicing/domain"; +import { IListInvoice_Participant_Response_DTO } from "@shared/contexts"; +import { InvoiceParticipantAddressPresenter } from "./InvoiceParticipantAddress.presenter"; + +export const InvoiceParticipantPresenter = ( + participant: IInvoiceParticipant, +): IListInvoice_Participant_Response_DTO => { + return { + participant_id: participant?.id?.toString(), + tin: participant?.tin?.toString(), + first_name: participant?.firstName?.toString(), + last_name: participant?.lastName?.toString(), + company_name: participant?.companyName?.toString(), + + billing_address: InvoiceParticipantAddressPresenter( + participant?.billingAddress!, + ), + shipping_address: InvoiceParticipantAddressPresenter( + participant?.shippingAddress!, + ), + }; +}; diff --git a/modules/invoices/src/server/presentation/controllers/list-invoices/presenter/InvoiceParticipantAddress.presenter.ts.bak b/modules/invoices/src/server/presentation/controllers/list-invoices/presenter/InvoiceParticipantAddress.presenter.ts.bak new file mode 100644 index 0000000..a30b900 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/list-invoices/presenter/InvoiceParticipantAddress.presenter.ts.bak @@ -0,0 +1,14 @@ +export const InvoiceParticipantAddressPresenter = ( + address: InvoiceParticipantAddress +): IListInvoice_AddressParticipant_Response_DTO => { + return { + address_id: address?.id.toString(), + street: address?.street.toString(), + city: address?.city.toString(), + postal_code: address?.postalCode.toString(), + province: address?.province.toString(), + country: address?.country.toString(), + email: address?.email.toString(), + phone: address?.phone.toString(), + }; +}; diff --git a/modules/invoices/src/server/presentation/controllers/list-invoices/presenter/index.ts b/modules/invoices/src/server/presentation/controllers/list-invoices/presenter/index.ts new file mode 100644 index 0000000..9ecb5c8 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/list-invoices/presenter/index.ts @@ -0,0 +1 @@ +export * from "./list-invoices.presenter"; diff --git a/modules/invoices/src/server/presentation/controllers/list-invoices/presenter/list-invoices.presenter.ts b/modules/invoices/src/server/presentation/controllers/list-invoices/presenter/list-invoices.presenter.ts new file mode 100644 index 0000000..0726831 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/list-invoices/presenter/list-invoices.presenter.ts @@ -0,0 +1,31 @@ +import { Invoice } from "#/server/domain"; +import { IListInvoicesResponseDTO } from "#/server/presentation/dto"; +import { Collection } from "@rdx/utils"; + +export interface IListInvoicesPresenter { + toDTO: (invoices: Collection) => IListInvoicesResponseDTO[]; +} + +export const listInvoicesPresenter: IListInvoicesPresenter = { + toDTO: (invoices: Collection): IListInvoicesResponseDTO[] => { + return invoices.map((invoice) => { + const result = { + id: invoice.id.toPrimitive(), + + invoice_status: invoice.status.toString(), + invoice_number: invoice.invoiceNumber.toString(), + invoice_series: invoice.invoiceSeries.toString(), + issue_date: invoice.issueDate.toISOString(), + operation_date: invoice.operationDate.toISOString(), + language_code: "ES", + currency: invoice.invoiceCurrency.toString(), + subtotal: invoice.calculateSubtotal().toPrimitive(), + total: invoice.calculateTotal().toPrimitive(), + + //recipient: InvoiceParticipantPresenter(invoice.recipient), + }; + + return result; + }); + }, +}; diff --git a/modules/invoices/src/server/presentation/controllers/update-invoice/index.ts.bak b/modules/invoices/src/server/presentation/controllers/update-invoice/index.ts.bak new file mode 100644 index 0000000..7dadbd9 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/update-invoice/index.ts.bak @@ -0,0 +1,57 @@ +import { IInvoicingContext } from "#/server/intrastructure"; +import { InvoiceRepository } from "#/server/intrastructure/Invoice.repository"; + +export const updateInvoiceController = (context: IInvoicingContext) => { + const adapter = context.adapter; + const repoManager = context.repositoryManager; + + repoManager.registerRepository("Invoice", (params = { transaction: null }) => { + const { transaction } = params; + + return new InvoiceRepository({ + transaction, + adapter, + mapper: createInvoiceMapper(context), + }); + }); + + repoManager.registerRepository("Participant", (params = { transaction: null }) => { + const { transaction } = params; + + return new InvoiceParticipantRepository({ + transaction, + adapter, + mapper: createInvoiceParticipantMapper(context), + }); + }); + + repoManager.registerRepository("ParticipantAddress", (params = { transaction: null }) => { + const { transaction } = params; + + return new InvoiceParticipantAddressRepository({ + transaction, + adapter, + mapper: createInvoiceParticipantAddressMapper(context), + }); + }); + + repoManager.registerRepository("Contact", (params = { transaction: null }) => { + const { transaction } = params; + + return new ContactRepository({ + transaction, + adapter, + mapper: createContactMapper(context), + }); + }); + + const updateInvoiceUseCase = new UpdateInvoiceUseCase(context); + + return new UpdateInvoiceController( + { + useCase: updateInvoiceUseCase, + presenter: updateInvoicePresenter, + }, + context + ); +}; diff --git a/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/InvoiceItem.presenter.ts.bak b/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/InvoiceItem.presenter.ts.bak new file mode 100644 index 0000000..3177298 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/InvoiceItem.presenter.ts.bak @@ -0,0 +1,19 @@ +import { InvoiceItem } from "@/contexts/invoicing/domain/InvoiceItems"; +import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; +import { ICollection, IMoney_Response_DTO } from "@shared/contexts"; + +export const invoiceItemPresenter = ( + items: ICollection, + context: IInvoicingContext +) => + items.totalCount > 0 + ? items.items.map((item: InvoiceItem) => ({ + description: item.description.toString(), + quantity: item.quantity.toString(), + unit_measure: "", + unit_price: item.unitPrice.toPrimitive() as IMoney_Response_DTO, + subtotal: item.calculateSubtotal().toPrimitive() as IMoney_Response_DTO, + tax_amount: item.calculateTaxAmount().toPrimitive() as IMoney_Response_DTO, + total: item.calculateTotal().toPrimitive() as IMoney_Response_DTO, + })) + : []; diff --git a/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/InvoiceParticipant.presenter.ts.bak b/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/InvoiceParticipant.presenter.ts.bak new file mode 100644 index 0000000..b416c7b --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/InvoiceParticipant.presenter.ts.bak @@ -0,0 +1,26 @@ +import { IInvoiceParticipant } from "@/contexts/invoicing/domain"; +import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; +import { IUpdateInvoice_Participant_Response_DTO } from "@shared/contexts"; +import { InvoiceParticipantAddressPresenter } from "./InvoiceParticipantAddress.presenter"; + +export const InvoiceParticipantPresenter = ( + participant: IInvoiceParticipant, + context: IInvoicingContext, +): IUpdateInvoice_Participant_Response_DTO | undefined => { + return { + id: participant.id.toString(), + tin: participant.tin.toString(), + first_name: participant.firstName.toString(), + last_name: participant.lastName.toString(), + company_name: participant.companyName.toString(), + + billing_address: InvoiceParticipantAddressPresenter( + participant.billingAddress!, + context, + ), + shipping_address: InvoiceParticipantAddressPresenter( + participant.shippingAddress!, + context, + ), + }; +}; diff --git a/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak b/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak new file mode 100644 index 0000000..376d491 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak @@ -0,0 +1,19 @@ +import { InvoiceParticipantAddress } from "@/contexts/invoicing/domain"; +import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; +import { IUpdateInvoice_AddressParticipant_Response_DTO } from "@shared/contexts"; + +export const InvoiceParticipantAddressPresenter = ( + address: InvoiceParticipantAddress, + context: IInvoicingContext, +): IUpdateInvoice_AddressParticipant_Response_DTO => { + return { + id: address.id.toString(), + street: address.street.toString(), + city: address.city.toString(), + postal_code: address.postalCode.toString(), + province: address.province.toString(), + country: address.country.toString(), + email: address.email.toString(), + phone: address.phone.toString(), + }; +}; diff --git a/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/UpdateInvoice.presenter.ts.bak b/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/UpdateInvoice.presenter.ts.bak new file mode 100644 index 0000000..bd0a981 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/UpdateInvoice.presenter.ts.bak @@ -0,0 +1,33 @@ +import { Invoice } from "@/contexts/invoicing/domain"; +import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; +import { IUpdateInvoice_Response_DTO } from "@shared/contexts"; +import { invoiceItemPresenter } from "./InvoiceItem.presenter"; +import { InvoiceParticipantPresenter } from "./InvoiceParticipant.presenter"; + +export interface IUpdateInvoicePresenter { + map: (invoice: Invoice, context: IInvoicingContext) => IUpdateInvoice_Response_DTO; +} + +export const updateInvoicePresenter: IUpdateInvoicePresenter = { + map: (invoice: Invoice, context: IInvoicingContext): IUpdateInvoice_Response_DTO => { + return { + id: invoice.id.toString(), + + invoice_status: invoice.status.toString(), + invoice_number: invoice.invoiceNumber.toString(), + invoice_series: invoice.invoiceSeries.toString(), + issue_date: invoice.issueDate.toISO8601(), + operation_date: invoice.operationDate.toISO8601(), + language_code: invoice.language.toString(), + currency: invoice.currency.toString(), + subtotal: invoice.calculateSubtotal().toPrimitive(), + total: invoice.calculateTotal().toPrimitive(), + + //sender: {}, //await InvoiceParticipantPresenter(invoice.senderId, context), + + recipient: InvoiceParticipantPresenter(invoice.recipient, context), + + items: invoiceItemPresenter(invoice.items, context), + }; + }, +}; diff --git a/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/index.ts.bak b/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/index.ts.bak new file mode 100644 index 0000000..88e907e --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/update-invoice/presenter/index.ts.bak @@ -0,0 +1 @@ +export * from "./UpdateInvoice.presenter"; diff --git a/modules/invoices/src/server/presentation/controllers/update-invoice/update-invoice.controller.ts.bak b/modules/invoices/src/server/presentation/controllers/update-invoice/update-invoice.controller.ts.bak new file mode 100644 index 0000000..26005e3 --- /dev/null +++ b/modules/invoices/src/server/presentation/controllers/update-invoice/update-invoice.controller.ts.bak @@ -0,0 +1,72 @@ +import { IInvoicingContext } from "#/server/intrastructure"; +import { ExpressController } from "@rdx/core"; +import { IUpdateInvoicePresenter } from "./presenter"; + +export class UpdateInvoiceController extends ExpressController { + private useCase: UpdateInvoiceUseCase2; + private presenter: IUpdateInvoicePresenter; + private context: IInvoicingContext; + + constructor( + props: { + useCase: UpdateInvoiceUseCase; + presenter: IUpdateInvoicePresenter; + }, + context: IInvoicingContext + ) { + super(); + + const { useCase, presenter } = props; + this.useCase = useCase; + this.presenter = presenter; + this.context = context; + } + + async executeImpl(): Promise { + const { invoiceId } = this.req.params; + const request: IUpdateInvoice_DTO = this.req.body; + + if (RuleValidator.validate(RuleValidator.RULE_NOT_NULL_OR_UNDEFINED, invoiceId).isFailure) { + return this.invalidInputError("Invoice Id param is required!"); + } + + const idOrError = UniqueID.create(invoiceId); + if (idOrError.isFailure) { + return this.invalidInputError("Invalid invoice Id param!"); + } + + try { + const result = await this.useCase.execute({ + id: idOrError.object, + data: request, + }); + + if (result.isFailure) { + const { error } = result; + + switch (error.code) { + case UseCaseError.NOT_FOUND_ERROR: + return this.notFoundError("Invoice not found", error); + + case UseCaseError.INVALID_INPUT_DATA: + return this.invalidInputError(error.message); + + case UseCaseError.UNEXCEPTED_ERROR: + return this.internalServerError(result.error.message, result.error); + + case UseCaseError.REPOSITORY_ERROR: + return this.conflictError(result.error, result.error.details); + + default: + return this.clientError(result.error.message); + } + } + + const invoice = result.object; + + return this.ok(this.presenter.map(invoice, this.context)); + } catch (e: unknown) { + return this.fail(e as IServerError); + } + } +} diff --git a/modules/invoices/src/server/presentation/dto/index.ts b/modules/invoices/src/server/presentation/dto/index.ts new file mode 100644 index 0000000..804ee56 --- /dev/null +++ b/modules/invoices/src/server/presentation/dto/index.ts @@ -0,0 +1,3 @@ +export * from "./invoices.request.dto"; +export * from "./invoices.response.dto"; +export * from "./invoices.schemas"; diff --git a/modules/invoices/src/server/presentation/dto/invoices.request.dto.ts b/modules/invoices/src/server/presentation/dto/invoices.request.dto.ts new file mode 100644 index 0000000..6964685 --- /dev/null +++ b/modules/invoices/src/server/presentation/dto/invoices.request.dto.ts @@ -0,0 +1,37 @@ +export interface IListInvoicesRequestDTO {} + +export interface ICreateInvoiceRequestDTO { + id: string; + + invoice_number: string; + invoice_series: string; + issue_date: string; + operation_date: string; + language_code: string; + currency: string; +} + +export interface IUpdateInvoiceRequestDTO { + is_freelancer: boolean; + name: string; + trade_name: string; + tin: string; + + street: string; + city: string; + state: string; + postal_code: string; + country: string; + + email: string; + phone: string; + fax: string; + website: string; + + legal_record: string; + + default_tax: number; + lang_code: string; + currency_code: string; + logo: string; +} diff --git a/modules/invoices/src/server/presentation/dto/invoices.response.dto.ts b/modules/invoices/src/server/presentation/dto/invoices.response.dto.ts new file mode 100644 index 0000000..b06b1e2 --- /dev/null +++ b/modules/invoices/src/server/presentation/dto/invoices.response.dto.ts @@ -0,0 +1,76 @@ +import { IMoneyDTO, IQuantityDTO } from "@rdx/core"; + +export interface IListInvoicesResponseDTO { + id: string; + + invoice_status: string; + invoice_number: string; + invoice_series: string; + issue_date: string; + operation_date: string; + language_code: string; + currency: string; + + subtotal: IMoneyDTO; + total: IMoneyDTO; +} + +export interface IGetInvoiceResponseDTO { + id: string; + + invoice_status: string; + invoice_number: string; + invoice_series: string; + issue_date: string; + operation_date: string; + language_code: string; + currency: string; + + subtotal: IMoneyDTO; + total: IMoneyDTO; + + items: { + description: string; + quantity: IQuantityDTO; + unit_measure: string; + unit_price: IMoneyDTO; + subtotal: IMoneyDTO; + //tax_amount: IMoneyDTO; + total: IMoneyDTO; + }[]; + + //customer: +} + +export interface ICreateInvoiceResponseDTO { + id: string; + + invoice_status: string; + invoice_number: string; + invoice_series: string; + issue_date: string; + operation_date: string; + language_code: string; + currency: string; + + subtotal: IMoneyDTO; + total: IMoneyDTO; +} + +// Inferir el tipo en TypeScript desde el esquema Zod +//export type IUpdateAcccountResponseDTO = z.infer; + +export interface IUpdateInvoiceResponseDTO { + id: string; + + invoice_status: string; + invoice_number: string; + invoice_series: string; + issue_date: string; + operation_date: string; + language_code: string; + currency: string; + + subtotal: IMoneyDTO; + total: IMoneyDTO; +} diff --git a/modules/invoices/src/server/presentation/dto/invoices.schemas.ts b/modules/invoices/src/server/presentation/dto/invoices.schemas.ts new file mode 100644 index 0000000..e07ec58 --- /dev/null +++ b/modules/invoices/src/server/presentation/dto/invoices.schemas.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; + +export const ICreateInvoiceRequestSchema = z.object({ + id: z.string().uuid(), + invoice_number: z.string().min(1), + invoice_series: z.string().min(1), + issue_date: z.string().refine((date) => { + const dateStr = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Invalid YYYY-MM-DD format"); + return dateStr.safeParse(date).success; + }), + operation_date: z.string().refine((date) => { + const dateStr = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Invalid YYYY-MM-DD format"); + return dateStr.safeParse(date).success; + }), + //customerId: z.string().uuid(), + lang_code: z.string().min(1), + currency_code: z.string().min(1), + + items: z + .array( + z.object({ + //id: z.string().uuid(), + description: z.string().optional(), + unit_price: z + .object({ + amount: z.number().positive(), + scale: z.number().positive(), + }) + .optional(), + quantity: z + .object({ + amount: z.number().positive(), + scale: z.number().positive(), + }) + .optional(), + }) + ) + .optional(), +}); + +export const IUpdateInvoiceRequestSchema = z.object({}); + +export const IDeleteInvoiceRequestSchema = z.object({}); diff --git a/modules/invoices/src/server/presentation/index.ts b/modules/invoices/src/server/presentation/index.ts new file mode 100644 index 0000000..a123289 --- /dev/null +++ b/modules/invoices/src/server/presentation/index.ts @@ -0,0 +1,2 @@ +export * from "./controllers"; +export * from "./dto"; diff --git a/modules/invoices/tsconfig.json b/modules/invoices/tsconfig.json new file mode 100644 index 0000000..69b627c --- /dev/null +++ b/modules/invoices/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["node", "jest"], + + "baseUrl": "./", + "paths": { + "#/*": ["./src/*"] + } + }, + "files": ["src/index.ts"], + "include": ["src/index.ts"], + "exclude": ["node_modules", "dist", "**/*/__tests__"] +} diff --git a/modules/invoices/turbo.json b/modules/invoices/turbo.json new file mode 100644 index 0000000..52a7d7a --- /dev/null +++ b/modules/invoices/turbo.json @@ -0,0 +1,9 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fbb210a --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "erp2025", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "turbo run build", + "clean": "turbo run clean && rm -rf node_modules", + "dev": "turbo run dev", + "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "lint": "turbo run lint", + "test": "turbo run test" + }, + "devDependencies": { + "prettier": "^3.5.3", + "turbo": "^2.5.2", + "eslint": "^9.25.1" + }, + "engines": { + "node": ">=22.13.1" + }, + "packageManager": "pnpm@10.9.0" +} diff --git a/packages/eslint-config/README.md b/packages/eslint-config/README.md new file mode 100644 index 0000000..8b42d90 --- /dev/null +++ b/packages/eslint-config/README.md @@ -0,0 +1,3 @@ +# `@turbo/eslint-config` + +Collection of internal eslint configurations. diff --git a/packages/eslint-config/library.js b/packages/eslint-config/library.js new file mode 100644 index 0000000..9b59cc0 --- /dev/null +++ b/packages/eslint-config/library.js @@ -0,0 +1,34 @@ +const { resolve } = require("node:path"); + +const project = resolve(process.cwd(), "tsconfig.json"); + +/** @type {import("eslint").Linter.Config} */ +module.exports = { + extends: ["eslint:recommended", "prettier", "turbo"], + plugins: ["only-warn"], + globals: { + React: true, + JSX: true, + }, + env: { + node: true, + }, + settings: { + "import/resolver": { + typescript: { + project, + }, + }, + }, + ignorePatterns: [ + // Ignore dotfiles + ".*.js", + "node_modules/", + "dist/", + ], + overrides: [ + { + files: ["*.js?(x)", "*.ts?(x)"], + }, + ], +}; diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json new file mode 100644 index 0000000..0c93275 --- /dev/null +++ b/packages/eslint-config/package.json @@ -0,0 +1,20 @@ +{ + "name": "@repo/eslint-config", + "version": "0.0.0", + "private": true, + "files": [ + "library.js", + "react-internal.js", + "server.js", + "vite.js" + ], + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.31.0", + "@typescript-eslint/parser": "^8.31.0", + "eslint-config-prettier": "^10.1.2", + "eslint-config-turbo": "^2.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-only-warn": "^1.1.0", + "typescript": "5.8.3" + } +} diff --git a/packages/eslint-config/react-internal.js b/packages/eslint-config/react-internal.js new file mode 100644 index 0000000..bf0a208 --- /dev/null +++ b/packages/eslint-config/react-internal.js @@ -0,0 +1,39 @@ +const { resolve } = require("node:path"); + +const project = resolve(process.cwd(), "tsconfig.json"); + +/* + * This is a custom ESLint configuration for use with + * internal (bundled by their consumer) libraries + * that utilize React. + */ + +/** @type {import("eslint").Linter.Config} */ +module.exports = { + extends: ["eslint:recommended", "prettier", "turbo"], + plugins: ["only-warn"], + globals: { + React: true, + JSX: true, + }, + env: { + browser: true, + }, + settings: { + "import/resolver": { + typescript: { + project, + }, + }, + }, + ignorePatterns: [ + // Ignore dotfiles + ".*.js", + "node_modules/", + "dist/", + ], + overrides: [ + // Force ESLint to detect .tsx files + { files: ["*.js?(x)", "*.ts?(x)"] }, + ], +}; diff --git a/packages/eslint-config/server.js b/packages/eslint-config/server.js new file mode 100644 index 0000000..4884374 --- /dev/null +++ b/packages/eslint-config/server.js @@ -0,0 +1,49 @@ +const { rules } = require("eslint-config-prettier"); + +module.exports = { + extends: ["eslint:recommended"], + env: { + node: true, + es6: true, + }, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + }, + overrides: [ + { + files: ["**/__tests__/**/*"], + env: { + jest: true, + }, + }, + ], + rules: { + "import/no-relative-parent-imports": "error", + "import/order": [ + "error", + { + groups: ["builtin", "external", "internal", ["parent", "sibling", "index"]], + pathGroups: [ + { + pattern: "@/**", + group: "internal", + }, + ], + pathGroupsExcludedImportTypes: ["builtin"], + "newlines-between": "always", + alphabetize: { + order: "asc", + caseInsensitive: true, + }, + }, + ], + }, + settings: { + "import/resolver": { + typescript: { + project, + }, + }, + }, +}; diff --git a/packages/eslint-config/vite.js b/packages/eslint-config/vite.js new file mode 100644 index 0000000..9417d1c --- /dev/null +++ b/packages/eslint-config/vite.js @@ -0,0 +1,19 @@ +module.exports = { + env: { + node: true, + }, + parser: "@typescript-eslint/parser", + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier", + ], + plugins: ["@typescript-eslint"], + parserOptions: { + sourceType: "module", + ecmaVersion: 2020, + }, + rules: { + "@typescript-eslint/no-non-null-assertion": "off", + }, +}; diff --git a/packages/jest-presets/node/jest-preset.ts b/packages/jest-presets/node/jest-preset.ts new file mode 100644 index 0000000..f043d01 --- /dev/null +++ b/packages/jest-presets/node/jest-preset.ts @@ -0,0 +1,17 @@ +import type { Config } from "jest"; + +const config = { + roots: [""], + transform: { + "^.+\\.tsx?$": "ts-jest", + }, + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + modulePathIgnorePatterns: [ + "/test/__fixtures__", + "/node_modules", + "/dist", + ], + preset: "ts-jest", +} as const satisfies Config; + +export default config; diff --git a/packages/jest-presets/package.json b/packages/jest-presets/package.json new file mode 100644 index 0000000..7f53438 --- /dev/null +++ b/packages/jest-presets/package.json @@ -0,0 +1,16 @@ +{ + "name": "@repo/jest-presets", + "version": "0.0.0", + "private": true, + "license": "MIT", + "files": [ + "node/jest-preset.ts" + ], + "scripts": { + "clean": "rm -rf dist && rm -rf node_modules" + }, + "dependencies": { + "jest": "^29.7.0", + "ts-jest": "^29.3.2" + } +} diff --git a/packages/rdx-auth/package.json b/packages/rdx-auth/package.json new file mode 100644 index 0000000..c2d26c5 --- /dev/null +++ b/packages/rdx-auth/package.json @@ -0,0 +1,81 @@ +{ + "name": "@rdx/auth", + "version": "0.0.0", + "private": true, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist && rm -rf node_modules", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "jest": { + "preset": "@repo/jest-presets/node" + }, + "devDependencies": { + "@jest/globals": "^29.7.0", + "@repo/eslint-config": "workspace:*", + "@repo/jest-presets": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/bcrypt": "^5.0.2", + "@types/body-parser": "^1.19.5", + "@types/cors": "^2.8.17", + "@types/dinero.js": "^1.9.4", + "@types/express": "^4.17.21", + "@types/glob": "^8.1.0", + "@types/jest": "^29.5.14", + "@types/jsonwebtoken": "^9.0.9", + "@types/luxon": "^3.6.2", + "@types/morgan": "^1.9.9", + "@types/node": "^22.15.2", + "@types/passport": "^1.0.17", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", + "@types/response-time": "^2.3.8", + "@types/supertest": "^6.0.3", + "@typescript-eslint/eslint-plugin": "^8.31.0", + "@typescript-eslint/parser": "^8.31.0", + "esbuild": "^0.25.3", + "esbuild-register": "^3.6.0", + "eslint": "^9.25.1", + "jest": "^29.7.0", + "nodemon": "^3.1.10", + "supertest": "^7.1.0", + "typescript": "5.8.3" + }, + "dependencies": { + "@rdx/core": "workspace:*", + "@rdx/ddd-domain": "workspace:*", + "@rdx/logger": "workspace:*", + "@rdx/modules": "workspace:*", + "@rdx/utils": "workspace:*", + "bcrypt": "^5.1.1", + "body-parser": "^2.2.0", + "cors": "^2.8.5", + "dinero.js": "^1.9.1", + "dotenv": "^16.5.0", + "express": "^4.21.2", + "helmet": "^8.1.0", + "http-status": "^2.1.0", + "jsonwebtoken": "^9.0.2", + "libphonenumber-js": "^1.11.20", + "luxon": "^3.5.0", + "module-alias": "^2.2.3", + "mysql2": "^3.12.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", + "path": "^0.12.7", + "reflect-metadata": "^0.2.2", + "response-time": "^2.3.3", + "sequelize": "^6.37.7", + "zod": "^3.24.3" + } +} diff --git a/packages/rdx-auth/src/application/index.ts b/packages/rdx-auth/src/application/index.ts new file mode 100644 index 0000000..fec328b --- /dev/null +++ b/packages/rdx-auth/src/application/index.ts @@ -0,0 +1,5 @@ +export * from "./list-users"; +export * from "./login"; +export * from "./logout"; +export * from "./refresh-token"; +export * from "./register"; diff --git a/packages/rdx-auth/src/application/list-users/index.ts b/packages/rdx-auth/src/application/list-users/index.ts new file mode 100644 index 0000000..47eeda4 --- /dev/null +++ b/packages/rdx-auth/src/application/list-users/index.ts @@ -0,0 +1 @@ +export * from "./list-users.use-case"; diff --git a/packages/rdx-auth/src/application/list-users/list-users.use-case.ts b/packages/rdx-auth/src/application/list-users/list-users.use-case.ts new file mode 100644 index 0000000..41c827c --- /dev/null +++ b/packages/rdx-auth/src/application/list-users/list-users.use-case.ts @@ -0,0 +1,16 @@ +import { IUserService, User } from "@/domain"; +import { ITransactionManager } from "@rdx/core"; +import { Collection, Result } from "@rdx/utils"; + +export class ListUsersUseCase { + constructor( + private readonly userService: IUserService, + private readonly transactionManager: ITransactionManager + ) {} + + public execute(): Promise, Error>> { + return this.transactionManager.complete((transaction) => { + return this.userService.findUsers(transaction); + }); + } +} diff --git a/packages/rdx-auth/src/application/login/index.ts b/packages/rdx-auth/src/application/login/index.ts new file mode 100644 index 0000000..71e9ada --- /dev/null +++ b/packages/rdx-auth/src/application/login/index.ts @@ -0,0 +1 @@ +export * from "./login.use-case"; diff --git a/packages/rdx-auth/src/application/login/login.use-case.ts b/packages/rdx-auth/src/application/login/login.use-case.ts new file mode 100644 index 0000000..685d161 --- /dev/null +++ b/packages/rdx-auth/src/application/login/login.use-case.ts @@ -0,0 +1,15 @@ +import { IAuthService, LoginData } from "@/domain"; +import { ITransactionManager } from "@rdx/core"; + +export class LoginUseCase { + constructor( + private readonly authService: IAuthService, + private readonly transactionManager: ITransactionManager + ) {} + + public async execute(loginData: LoginData) { + return await this.transactionManager.complete(async (transaction) => { + return await this.authService.loginUser(loginData, transaction); + }); + } +} diff --git a/packages/rdx-auth/src/application/logout/index.ts b/packages/rdx-auth/src/application/logout/index.ts new file mode 100644 index 0000000..c7dcefc --- /dev/null +++ b/packages/rdx-auth/src/application/logout/index.ts @@ -0,0 +1 @@ +export * from "./logout.use-case"; diff --git a/packages/rdx-auth/src/application/logout/logout.use-case.ts b/packages/rdx-auth/src/application/logout/logout.use-case.ts new file mode 100644 index 0000000..5782c3d --- /dev/null +++ b/packages/rdx-auth/src/application/logout/logout.use-case.ts @@ -0,0 +1,15 @@ +import { IAuthService, LogoutData } from "@/domain"; +import { ITransactionManager } from "@rdx/core"; + +export class LogoutUseCase { + constructor( + private readonly authService: IAuthService, + private readonly transactionManager: ITransactionManager + ) {} + + public async execute(logoutData: LogoutData) { + return await this.transactionManager.complete(async (transaction) => { + return await this.authService.logoutUser(logoutData, transaction); + }); + } +} diff --git a/packages/rdx-auth/src/application/refresh-token/index.ts b/packages/rdx-auth/src/application/refresh-token/index.ts new file mode 100644 index 0000000..e1939e9 --- /dev/null +++ b/packages/rdx-auth/src/application/refresh-token/index.ts @@ -0,0 +1 @@ +export * from "./refresh-token.use-case"; diff --git a/packages/rdx-auth/src/application/refresh-token/refresh-token.use-case.ts b/packages/rdx-auth/src/application/refresh-token/refresh-token.use-case.ts new file mode 100644 index 0000000..fec5f5c --- /dev/null +++ b/packages/rdx-auth/src/application/refresh-token/refresh-token.use-case.ts @@ -0,0 +1,22 @@ +import { IAuthService, Token } from "@/domain"; +import { ITransactionManager } from "@rdx/core"; +export class RefreshTokenUseCase { + constructor( + private readonly authService: IAuthService, + private readonly transactionManager: ITransactionManager + ) {} + + public async execute(token: Token) { + return await this.transactionManager.complete(async (transaction) => { + const payloadData = this.authService.verifyRefreshToken(token); + + /*if (!payload || !payload.email || !payload.user_id || !payload.tab_id || !payload.roles) { + return Result.fail(new Error("Invalid input data")); + }*/ + + return this.authService.generateRefreshToken({ + ...payloadData, + }); + }); + } +} diff --git a/packages/rdx-auth/src/application/register/index.ts b/packages/rdx-auth/src/application/register/index.ts new file mode 100644 index 0000000..8d88027 --- /dev/null +++ b/packages/rdx-auth/src/application/register/index.ts @@ -0,0 +1 @@ +export * from "./register.use-case"; diff --git a/packages/rdx-auth/src/application/register/register.use-case.ts b/packages/rdx-auth/src/application/register/register.use-case.ts new file mode 100644 index 0000000..81d6a06 --- /dev/null +++ b/packages/rdx-auth/src/application/register/register.use-case.ts @@ -0,0 +1,22 @@ +import { IAuthService, RegisterData } from "@/domain"; +import { ITransactionManager } from "@rdx/core"; +import { logger } from "@rdx/logger"; +import { Result } from "@rdx/utils"; + +export class RegisterUseCase { + constructor( + private readonly authService: IAuthService, + private readonly transactionManager: ITransactionManager + ) {} + + public async execute(registerData: RegisterData) { + return await this.transactionManager.complete(async (transaction) => { + try { + return await this.authService.registerUser(registerData, transaction); + } catch (error: unknown) { + logger.error(error as Error); + return Result.fail(error as Error); + } + }); + } +} diff --git a/packages/rdx-auth/src/domain/aggregates/authenticated-user.ts b/packages/rdx-auth/src/domain/aggregates/authenticated-user.ts new file mode 100644 index 0000000..e7c8270 --- /dev/null +++ b/packages/rdx-auth/src/domain/aggregates/authenticated-user.ts @@ -0,0 +1,90 @@ +import { AggregateRoot, EmailAddress, UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { UserAuthenticatedEvent } from "../events"; +import { HashPassword, PlainPassword, Username } from "../value-objects"; + +export interface IAuthenticatedUserProps { + username: Username; + email: EmailAddress; + hashPassword: HashPassword; + roles: string[]; +} + +export interface IAuthenticatedUser { + username: Username; + email: EmailAddress; + hashPassword: HashPassword; + + accessToken: string; + refreshToken: string; + + isUser: boolean; + isAdmin: boolean; + + verifyPassword(candidatePassword: PlainPassword): Promise; + hasRole(role: string): boolean; + hasRoles(roles: string[]): boolean; + getRoles(): string[]; + toPersistenceData(): any; +} + +export class AuthenticatedUser + extends AggregateRoot + implements IAuthenticatedUser +{ + public accessToken: string = ""; + public refreshToken: string = ""; + + static create(props: IAuthenticatedUserProps, id: UniqueID): Result { + const user = new AuthenticatedUser(props, id); + + // 🔹 Disparar evento de dominio "UserAuthenticatedEvent" + const { email } = props; + user.addDomainEvent(new UserAuthenticatedEvent(id, email.toString())); + + return Result.ok(user); + } + + verifyPassword(candidatePassword: PlainPassword): Promise { + return this.props.hashPassword.verifyPassword(candidatePassword.toString()); + } + + getRoles(): string[] { + return this.props.roles; + } + + hasRole(role: string): boolean { + return (this.props.roles || []).some((r) => r === role); + } + + hasRoles(roles: string[]): boolean { + return roles && roles.map((rol) => this.hasRole(rol)).some((value) => value != false); + } + + get username(): Username { + return this.props.username; + } + + get email(): EmailAddress { + return this.props.email; + } + + get hashPassword(): HashPassword { + return this.props.hashPassword; + } + + get isUser(): boolean { + return this.hasRole("user"); + } + + get isAdmin(): boolean { + return this.hasRole("admin"); + } + + /** + * 🔹 Devuelve una representación lista para persistencia + */ + toPersistenceData(): any { + return; + } +} diff --git a/packages/rdx-auth/src/domain/aggregates/index.ts b/packages/rdx-auth/src/domain/aggregates/index.ts new file mode 100644 index 0000000..a20563f --- /dev/null +++ b/packages/rdx-auth/src/domain/aggregates/index.ts @@ -0,0 +1,4 @@ +export * from "./authenticated-user"; + +export * from "./role"; +export * from "./user"; diff --git a/packages/rdx-auth/src/domain/aggregates/role.ts b/packages/rdx-auth/src/domain/aggregates/role.ts new file mode 100644 index 0000000..420c0d8 --- /dev/null +++ b/packages/rdx-auth/src/domain/aggregates/role.ts @@ -0,0 +1,15 @@ +import { AggregateRoot, UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; + +export interface IRoleProps {} + +export interface IRole {} + +export class Role extends AggregateRoot implements IRole { + static create(props: IRoleProps, id: UniqueID): Result { + const role = new Role(props, id); + return Result.ok(role); + } + + toPersistenceData(): any {} +} diff --git a/packages/rdx-auth/src/domain/aggregates/user.ts b/packages/rdx-auth/src/domain/aggregates/user.ts new file mode 100644 index 0000000..7c64b4f --- /dev/null +++ b/packages/rdx-auth/src/domain/aggregates/user.ts @@ -0,0 +1,67 @@ +import { AggregateRoot, EmailAddress, UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { UserAuthenticatedEvent } from "../events"; +import { Username } from "../value-objects"; + +export interface IUserProps { + username: Username; + email: EmailAddress; + roles: string[]; +} + +export interface IUser { + username: Username; + email: EmailAddress; + + isUser: boolean; + isAdmin: boolean; + isActive: boolean; + + hasRole(role: string): boolean; + hasRoles(roles: string[]): boolean; + getRoles(): string[]; +} + +export class User extends AggregateRoot implements IUser { + static create(props: IUserProps, id: UniqueID): Result { + const user = new User(props, id); + + // 🔹 Disparar evento de dominio "UserAuthenticatedEvent" + const { email } = props; + user.addDomainEvent(new UserAuthenticatedEvent(id, email.toString())); + + return Result.ok(user); + } + + getRoles(): string[] { + return this.props.roles; + } + + hasRole(role: string): boolean { + return (this.props.roles || []).some((r) => r === role); + } + + hasRoles(roles: string[]): boolean { + return roles.map((rol) => this.hasRole(rol)).some((value) => value != false); + } + + get username(): Username { + return this.props.username; + } + + get email(): EmailAddress { + return this.props.email; + } + + get isUser(): boolean { + return this.hasRole("user"); + } + + get isAdmin(): boolean { + return this.hasRole("admin"); + } + + get isActive(): boolean { + return true; + } +} diff --git a/packages/rdx-auth/src/domain/entities/index.ts b/packages/rdx-auth/src/domain/entities/index.ts new file mode 100644 index 0000000..a66561a --- /dev/null +++ b/packages/rdx-auth/src/domain/entities/index.ts @@ -0,0 +1,5 @@ +export * from "./jwt-payload"; +export * from "./login-data"; +export * from "./logout-data"; +export * from "./register-data"; +export * from "./tab-context"; diff --git a/packages/rdx-auth/src/domain/entities/jwt-payload.ts b/packages/rdx-auth/src/domain/entities/jwt-payload.ts new file mode 100644 index 0000000..fb352e0 --- /dev/null +++ b/packages/rdx-auth/src/domain/entities/jwt-payload.ts @@ -0,0 +1,67 @@ +import { DomainEntity, EmailAddress, UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; + +export interface IJWTPayloadProps { + tabId: UniqueID; + userId: UniqueID; + email: EmailAddress; +} + +export interface IJWTPayloadPrimitives { + tab_id: string; + user_id: string; + email: string; +} + +export interface IJWTPayload { + tabId: UniqueID; + userId: UniqueID; + email: EmailAddress; + + toPersistenceData(): any; +} + +export class JWTPayload extends DomainEntity implements IJWTPayload { + static create(props: IJWTPayloadProps): Result { + return Result.ok(new JWTPayload(props)); + } + + static createFromPrimitives(values: IJWTPayloadPrimitives): Result { + const { email, user_id, tab_id } = values; + const emailOrError = EmailAddress.create(email); + const userIdOrError = UniqueID.create(user_id, false); + const tabIdOrError = UniqueID.create(tab_id, false); + + const result = Result.combine([emailOrError, userIdOrError, tabIdOrError]); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return JWTPayload.create({ + email: emailOrError.data, + userId: userIdOrError.data, + tabId: tabIdOrError.data, + }); + } + + get tabId(): UniqueID { + return this.props.tabId; + } + + get userId(): UniqueID { + return this.props.userId; + } + + get email(): EmailAddress { + return this.props.email; + } + + toPersistenceData(): any { + return { + tab_id: this.tabId.toString(), + user_id: this.userId.toString(), + email: this.email.toString(), + }; + } +} diff --git a/packages/rdx-auth/src/domain/entities/login-data.ts b/packages/rdx-auth/src/domain/entities/login-data.ts new file mode 100644 index 0000000..9c7fe32 --- /dev/null +++ b/packages/rdx-auth/src/domain/entities/login-data.ts @@ -0,0 +1,58 @@ +import { DomainEntity, EmailAddress, UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { PlainPassword } from "../value-objects"; + +export interface ILoginDataProps { + email: EmailAddress; + plainPassword: PlainPassword; + tabId: UniqueID; +} + +export interface ILoginDataPrimitives { + email: string; + plainPassword: string; + tabId: string; +} + +export interface ILoginData { + email: EmailAddress; + plainPassword: PlainPassword; + tabId: UniqueID; +} + +export class LoginData extends DomainEntity implements ILoginData { + static create(props: ILoginDataProps): Result { + return Result.ok(new this(props)); + } + + static createFromPrimitives(values: ILoginDataPrimitives): Result { + const { email, plainPassword, tabId } = values; + const emailOrError = EmailAddress.create(email); + const plainPasswordOrError = PlainPassword.create(plainPassword); + const tabIdOrError = UniqueID.create(tabId, false); + + const result = Result.combine([emailOrError, plainPasswordOrError, tabIdOrError]); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return LoginData.create({ + email: emailOrError.data, + plainPassword: plainPasswordOrError.data, + tabId: tabIdOrError.data, + }); + } + + get email(): EmailAddress { + return this.props.email; + } + + get plainPassword(): PlainPassword { + return this.props.plainPassword; + } + + get tabId(): UniqueID { + return this.props.tabId; + } +} diff --git a/packages/rdx-auth/src/domain/entities/logout-data.ts b/packages/rdx-auth/src/domain/entities/logout-data.ts new file mode 100644 index 0000000..a8ce332 --- /dev/null +++ b/packages/rdx-auth/src/domain/entities/logout-data.ts @@ -0,0 +1,48 @@ +import { DomainEntity, EmailAddress, UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; + +export interface ILogoutDataProps { + email: EmailAddress; + tabId: UniqueID; +} + +export interface ILogoutDataPrimitives { + email: string; + tabId: string; +} + +export interface ILogoutData { + email: EmailAddress; + tabId: UniqueID; +} + +export class LogoutData extends DomainEntity implements ILogoutData { + static create(props: ILogoutDataProps): Result { + return Result.ok(new this(props)); + } + + static createFromPrimitives(values: ILogoutDataPrimitives): Result { + const { email, tabId } = values; + const emailOrError = EmailAddress.create(email); + const tabIdOrError = UniqueID.create(tabId, false); + + const result = Result.combine([emailOrError, tabIdOrError]); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return LogoutData.create({ + email: emailOrError.data, + tabId: tabIdOrError.data, + }); + } + + get email(): EmailAddress { + return this.props.email; + } + + get tabId(): UniqueID { + return this.props.tabId; + } +} diff --git a/packages/rdx-auth/src/domain/entities/register-data.ts b/packages/rdx-auth/src/domain/entities/register-data.ts new file mode 100644 index 0000000..209c6ca --- /dev/null +++ b/packages/rdx-auth/src/domain/entities/register-data.ts @@ -0,0 +1,59 @@ +import { DomainEntity, EmailAddress } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { HashPassword, Username } from "../value-objects"; + +export interface IRegisterDataProps { + username: Username; + email: EmailAddress; + hashPassword: HashPassword; +} + +export interface IRegisterDataPrimitives { + username: string; + email: string; + plainPassword: string; +} + +export interface IRegisterData { + username: Username; + email: EmailAddress; + hashPassword: HashPassword; +} + +export class RegisterData extends DomainEntity implements IRegisterData { + static create(props: IRegisterDataProps): Result { + return Result.ok(new this(props)); + } + + static createFromPrimitives(props: IRegisterDataPrimitives): Result { + const { username, email, plainPassword } = props; + + const userNameOrError = Username.create(username); + const emailOrError = EmailAddress.create(email); + const hashPasswordOrError = HashPassword.createFromPlainText(plainPassword); + + const result = Result.combine([userNameOrError, emailOrError, hashPasswordOrError]); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return RegisterData.create({ + username: userNameOrError.data, + email: emailOrError.data, + hashPassword: hashPasswordOrError.data, + }); + } + + get username(): Username { + return this.props.username; + } + + get email(): EmailAddress { + return this.props.email; + } + + get hashPassword(): HashPassword { + return this.props.hashPassword; + } +} diff --git a/packages/rdx-auth/src/domain/entities/tab-context.ts b/packages/rdx-auth/src/domain/entities/tab-context.ts new file mode 100644 index 0000000..4728d44 --- /dev/null +++ b/packages/rdx-auth/src/domain/entities/tab-context.ts @@ -0,0 +1,49 @@ +import { DomainEntity, UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; + +export interface ITabContextProps { + tabId: UniqueID; + userId: UniqueID; +} + +export interface ITabContextPrimitives { + id: string; + tab_id: string; + user_id: string; +} + +export interface ITabContext { + tabId: UniqueID; + userId: UniqueID; +} + +export class TabContext extends DomainEntity implements ITabContext { + static create(props: ITabContextProps, id?: UniqueID): Result { + return Result.ok(new this(props, id)); + } + + static createFromPrimitives(values: ITabContextPrimitives): Result { + const { user_id, tab_id } = values; + const userIdOrError = UniqueID.create(user_id, false); + const tabIdOrError = UniqueID.create(tab_id, false); + + const result = Result.combine([userIdOrError, tabIdOrError]); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return TabContext.create({ + userId: userIdOrError.data, + tabId: tabIdOrError.data, + }); + } + + get tabId(): UniqueID { + return this.props.tabId; + } + + get userId(): UniqueID { + return this.props.userId; + } +} diff --git a/packages/rdx-auth/src/domain/events/index.ts b/packages/rdx-auth/src/domain/events/index.ts new file mode 100644 index 0000000..c228d2e --- /dev/null +++ b/packages/rdx-auth/src/domain/events/index.ts @@ -0,0 +1 @@ +export * from "./user-authenticated.event"; diff --git a/packages/rdx-auth/src/domain/events/user-authenticated.event.ts b/packages/rdx-auth/src/domain/events/user-authenticated.event.ts new file mode 100644 index 0000000..7bfa19b --- /dev/null +++ b/packages/rdx-auth/src/domain/events/user-authenticated.event.ts @@ -0,0 +1,13 @@ +import { IDomainEvent, UniqueID } from "@rdx/ddd-domain"; + +export class UserAuthenticatedEvent implements IDomainEvent { + public readonly eventName = "UserAuthenticated"; + public readonly occurredAt: Date; + + constructor( + public readonly aggregateId: UniqueID, + public readonly email: string // Email en formato string + ) { + this.occurredAt = new Date(); + } +} diff --git a/packages/rdx-auth/src/domain/index.ts b/packages/rdx-auth/src/domain/index.ts new file mode 100644 index 0000000..67ca20f --- /dev/null +++ b/packages/rdx-auth/src/domain/index.ts @@ -0,0 +1,6 @@ +export * from "./aggregates"; +export * from "./entities"; +export * from "./events"; +export * from "./repositories"; +export * from "./services"; +export * from "./value-objects"; diff --git a/packages/rdx-auth/src/domain/repositories/authenticated-user-repository.interface.ts b/packages/rdx-auth/src/domain/repositories/authenticated-user-repository.interface.ts new file mode 100644 index 0000000..f36d63b --- /dev/null +++ b/packages/rdx-auth/src/domain/repositories/authenticated-user-repository.interface.ts @@ -0,0 +1,14 @@ +import { EmailAddress } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { AuthenticatedUser } from "../aggregates"; +import { Username } from "../value-objects"; + +export interface IAuthenticatedUserRepository { + getUserByEmail(email: EmailAddress, transaction?: any): Promise>; + userExists( + username: Username, + email: EmailAddress, + transaction?: any + ): Promise>; + createUser(user: AuthenticatedUser, transaction?: any): Promise>; +} diff --git a/packages/rdx-auth/src/domain/repositories/index.ts b/packages/rdx-auth/src/domain/repositories/index.ts new file mode 100644 index 0000000..b5f0a9e --- /dev/null +++ b/packages/rdx-auth/src/domain/repositories/index.ts @@ -0,0 +1,4 @@ +export * from "./authenticated-user-repository.interface"; +export * from "./tab-context-repository.interface"; +export * from "./user-permission-repository.interface"; +export * from "./user-repository.interface"; diff --git a/packages/rdx-auth/src/domain/repositories/tab-context-repository.interface.ts b/packages/rdx-auth/src/domain/repositories/tab-context-repository.interface.ts new file mode 100644 index 0000000..fb2c660 --- /dev/null +++ b/packages/rdx-auth/src/domain/repositories/tab-context-repository.interface.ts @@ -0,0 +1,20 @@ +import { UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { Transaction } from "sequelize"; +import { TabContext } from "../entities"; + +export interface ITabContextRepository { + getContextByTabId(tabId: UniqueID, transaction?: any): Promise>; + + contextExistsByTabId(tabId: UniqueID, transaction?: any): Promise>; + + registerContextByTabId( + context: TabContext, + transaction?: Transaction + ): Promise>; + + unregisterContextByTabId( + context: TabContext, + transaction?: Transaction + ): Promise>; +} diff --git a/packages/rdx-auth/src/domain/repositories/user-permission-repository.interface.ts b/packages/rdx-auth/src/domain/repositories/user-permission-repository.interface.ts new file mode 100644 index 0000000..b64bffb --- /dev/null +++ b/packages/rdx-auth/src/domain/repositories/user-permission-repository.interface.ts @@ -0,0 +1 @@ +export interface IUserPermissionRepository {} diff --git a/packages/rdx-auth/src/domain/repositories/user-repository.interface.ts b/packages/rdx-auth/src/domain/repositories/user-repository.interface.ts new file mode 100644 index 0000000..b0dafb8 --- /dev/null +++ b/packages/rdx-auth/src/domain/repositories/user-repository.interface.ts @@ -0,0 +1,9 @@ +import { EmailAddress, UniqueID } from "@rdx/ddd-domain"; +import { Collection, Result } from "@rdx/utils"; +import { User } from "../aggregates"; + +export interface IUserRepository { + findAll(transaction?: any): Promise, Error>>; + findById(id: UniqueID, transaction?: any): Promise>; + findByEmail(email: EmailAddress, transaction?: any): Promise>; +} diff --git a/packages/rdx-auth/src/domain/services/auth-service.interface.ts b/packages/rdx-auth/src/domain/services/auth-service.interface.ts new file mode 100644 index 0000000..262887e --- /dev/null +++ b/packages/rdx-auth/src/domain/services/auth-service.interface.ts @@ -0,0 +1,42 @@ +import { EmailAddress } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { + AuthenticatedUser, + IJWTPayload, + LoginData, + LogoutData, + RegisterData, + TabContext, + Token, +} from ".."; + +export interface IAuthService { + generateAccessToken(payload: IJWTPayload): Result; + generateRefreshToken(payload: IJWTPayload): Result; + verifyRefreshToken(token: Token): IJWTPayload; + + registerUser( + registerData: RegisterData, + transaction?: any + ): Promise>; + + loginUser( + loginData: LoginData, + transaction?: any + ): Promise< + Result< + { + user: AuthenticatedUser; + tabContext: TabContext; + tokens: { + accessToken: Token; + refreshToken: Token; + }; + }, + Error + > + >; + + logoutUser(logoutData: LogoutData, transaction?: any): Promise>; + getUserByEmail(email: EmailAddress, transaction?: any): Promise>; +} diff --git a/packages/rdx-auth/src/domain/services/auth.service.ts b/packages/rdx-auth/src/domain/services/auth.service.ts new file mode 100644 index 0000000..6d6b88a --- /dev/null +++ b/packages/rdx-auth/src/domain/services/auth.service.ts @@ -0,0 +1,204 @@ +import { EmailAddress, UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { + AuthenticatedUser, + IAuthenticatedUserRepository, + IJWTPayload, + JWTPayload, + LoginData, + RegisterData, + TabContext, + Token, +} from ".."; +import { JwtHelper } from "../../infraestructure/passport/jwt.helper"; +import { ITabContextRepository } from "../repositories/tab-context-repository.interface"; +import { IAuthService } from "./auth-service.interface"; + +const ACCESS_EXPIRATION = process.env.JWT_ACCESS_EXPIRATION || "1h"; +const REFRESH_EXPIRATION = process.env.JWT_REFRESH_EXPIRATION || "7d"; + +export class AuthService implements IAuthService { + constructor( + private readonly authUserRepo: IAuthenticatedUserRepository, + private readonly tabContextRepo: ITabContextRepository + ) {} + + generateAccessToken(payload: IJWTPayload): Result { + const data = payload.toPersistenceData(); + return Token.create(JwtHelper.generateToken(data, ACCESS_EXPIRATION)); + } + + generateRefreshToken(payload: IJWTPayload): Result { + const data = payload.toPersistenceData(); + return Token.create(JwtHelper.generateToken(data, REFRESH_EXPIRATION)); + } + + verifyRefreshToken(token: Token): IJWTPayload { + return JwtHelper.verifyToken(token.toString()); + } + + /** + * + * Registra un nuevo usuario en la base de datos bajo transacción. + */ + async registerUser( + registerData: RegisterData, + transaction?: any + ): Promise> { + const { username, email, hashPassword } = registerData; + + // Verificar si el usuario ya existe + const userExists = await this.authUserRepo.userExists(username, email, transaction); + if (userExists.isSuccess && userExists.data) { + return Result.fail(new Error("Email is already registered")); + } + + const newUserId = UniqueID.generateNewID().data; + + const userOrError = AuthenticatedUser.create( + { + username, + email, + hashPassword, + roles: ["USER"], + }, + newUserId + ); + + if (userOrError.isFailure) { + return Result.fail(userOrError.error); + } + + const createdResult = await this.authUserRepo.createUser(userOrError.data, transaction); + + if (createdResult.isFailure) { + return Result.fail(createdResult.error); + } + + return Result.ok(userOrError.data); + } + + /** + * + * Autentica a un usuario validando su email y contraseña. + */ + async loginUser( + loginData: LoginData, + transaction?: any + ): Promise< + Result< + { + user: AuthenticatedUser; + tabContext: TabContext; + tokens: { + accessToken: Token; + refreshToken: Token; + }; + }, + Error + > + > { + let result: any; + const { email, plainPassword, tabId } = loginData; + + // 🔹 Verificar si el usuario existe en la base de datos + result = await this.authUserRepo.getUserByEmail(email, transaction); + if (result.isFailure) { + return Result.fail(new Error("Invalid email or password")); + } + + const user = result.data; + + // 🔹 Verificar que la contraseña sea correcta + const isValidPassword = await user.verifyPassword(plainPassword); + if (!isValidPassword) { + return Result.fail(new Error("Invalid email or password")); + } + + // Registrar o actualizar el contexto de ese tab ID + const contextOrError = TabContext.create({ + userId: user.id, + tabId: tabId, + }); + + // 🔹 Generar Access Token y Refresh Token + const payloadOrError = JWTPayload.create({ + userId: user.id, + email: email, + tabId: tabId, + //roles: ["USER"], + }); + + result = Result.combine([contextOrError, payloadOrError]); + if (result.isFailure) { + return Result.fail(new Error("Error on login")); + } + + const tabContext = contextOrError.data; + await this.tabContextRepo.registerContextByTabId(tabContext, transaction); + + const accessTokenOrError = this.generateAccessToken(payloadOrError.data); + const refreshTokenOrError = this.generateRefreshToken(payloadOrError.data); + + result = Result.combine([accessTokenOrError, refreshTokenOrError]); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return Result.ok({ + user, + tabContext, + tokens: { + accessToken: accessTokenOrError.data, + refreshToken: refreshTokenOrError.data, + }, + }); + } + + /** + * + * Autentica a un usuario validando su email y contraseña. + */ + async logoutUser( + params: { email: EmailAddress; tabId: UniqueID }, + transaction?: any + ): Promise> { + const { email, tabId } = params; + + // 🔹 Verificar si el usuario existe en la base de datos + const userResult = await this.authUserRepo.getUserByEmail(email, transaction); + if (userResult.isFailure) { + return Result.fail(new Error("Invalid email or password")); + } + + const user = userResult.data; + + const contextOrError = TabContext.create({ + userId: user.id, + tabId: tabId, + }); + + if (contextOrError.isFailure) { + return Result.fail(new Error("Error creating user context")); + } + + // Desregistrar el contexto de ese tab ID + await this.tabContextRepo.unregisterContextByTabId(contextOrError.data, transaction); + + return Result.ok(); + } + + async getUserByEmail( + email: EmailAddress, + transaction?: any + ): Promise> { + const userResult = await this.authUserRepo.getUserByEmail(email, transaction); + + if (userResult.isFailure || !userResult.data) { + return Result.fail(new Error("Invalid email or password")); + } + + return Result.ok(userResult.data); + } +} diff --git a/packages/rdx-auth/src/domain/services/index.ts b/packages/rdx-auth/src/domain/services/index.ts new file mode 100644 index 0000000..abde394 --- /dev/null +++ b/packages/rdx-auth/src/domain/services/index.ts @@ -0,0 +1,8 @@ +export * from "./auth-service.interface"; +export * from "./auth.service"; + +export * from "./tab-context-service.interface"; +export * from "./tab-context.service"; + +export * from "./user-service.interface"; +export * from "./user.service"; diff --git a/packages/rdx-auth/src/domain/services/tab-context-service.interface.ts b/packages/rdx-auth/src/domain/services/tab-context-service.interface.ts new file mode 100644 index 0000000..4fff4f7 --- /dev/null +++ b/packages/rdx-auth/src/domain/services/tab-context-service.interface.ts @@ -0,0 +1,15 @@ +import { UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { TabContext } from "../entities"; + +export interface ITabContextService { + getContextByTabId(tabId: UniqueID, transaction?: any): Promise>; + createContext( + params: { tabId: UniqueID; userId: UniqueID }, + transaction?: any + ): Promise>; + removeContext( + params: { tabId: UniqueID; userId: UniqueID }, + transaction?: any + ): Promise>; +} diff --git a/packages/rdx-auth/src/domain/services/tab-context.service.ts b/packages/rdx-auth/src/domain/services/tab-context.service.ts new file mode 100644 index 0000000..d35b98a --- /dev/null +++ b/packages/rdx-auth/src/domain/services/tab-context.service.ts @@ -0,0 +1,87 @@ +import { UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { TabContext } from "../entities"; +import { ITabContextRepository } from "../repositories"; +import { ITabContextService } from "./tab-context-service.interface"; + +export class TabContextService implements ITabContextService { + constructor(private readonly tabContextRepo: ITabContextRepository) {} + + /** + * Obtiene el contexto de una pestaña por su ID + */ + async getContextByTabId(tabId: UniqueID, transaction?: any): Promise> { + // Verificar si la pestaña existe + const tabContextOrError = await this.tabContextRepo.getContextByTabId(tabId, transaction); + if (tabContextOrError.isSuccess && !tabContextOrError.data) { + return Result.fail(new Error("Invalid or expired Tab ID")); + } + + if (tabContextOrError.isFailure) { + return Result.fail(tabContextOrError.error); + } + + return Result.ok(tabContextOrError.data); + } + + /** + * Registra un nuevo contexto de pestaña para un usuario + */ + async createContext( + params: { + tabId: UniqueID; + userId: UniqueID; + }, + transaction?: any + ): Promise> { + const { tabId, userId } = params; + + if (!userId || !tabId) { + return Result.fail(new Error("User ID and Tab ID are required")); + } + + const contextOrError = TabContext.create( + { + userId, + tabId, + }, + UniqueID.generateNewID().data + ); + + if (contextOrError.isFailure) { + return Result.fail(contextOrError.error); + } + + await this.tabContextRepo.registerContextByTabId(contextOrError.data, transaction); + + return Result.ok(contextOrError.data); + } + + /** + * Elimina un contexto de pestaña por su ID + */ + async removeContext( + params: { tabId: UniqueID; userId: UniqueID }, + transaction?: any + ): Promise> { + const { tabId, userId } = params; + + if (!userId || !tabId) { + return Result.fail(new Error("User ID and Tab ID are required")); + } + + const contextOrError = TabContext.create( + { + userId, + tabId, + }, + UniqueID.generateNewID().data + ); + + if (contextOrError.isFailure) { + return Result.fail(contextOrError.error); + } + + return await this.tabContextRepo.unregisterContextByTabId(contextOrError.data, transaction); + } +} diff --git a/packages/rdx-auth/src/domain/services/user-service.interface.ts b/packages/rdx-auth/src/domain/services/user-service.interface.ts new file mode 100644 index 0000000..ebeb202 --- /dev/null +++ b/packages/rdx-auth/src/domain/services/user-service.interface.ts @@ -0,0 +1,8 @@ +import { UniqueID } from "@rdx/ddd-domain"; +import { Collection, Result } from "@rdx/utils"; +import { User } from "../aggregates"; + +export interface IUserService { + findUsers(transaction?: any): Promise, Error>>; + findUserById(userId: UniqueID, transaction?: any): Promise>; +} diff --git a/packages/rdx-auth/src/domain/services/user.service.ts b/packages/rdx-auth/src/domain/services/user.service.ts new file mode 100644 index 0000000..56a7055 --- /dev/null +++ b/packages/rdx-auth/src/domain/services/user.service.ts @@ -0,0 +1,40 @@ +import { UniqueID } from "@rdx/ddd-domain"; +import { Collection, Result } from "@rdx/utils"; +import { IUserRepository, User } from ".."; +import { IUserService } from "./user-service.interface"; + +export class UserService implements IUserService { + constructor(private readonly userRepository: IUserRepository) {} + + async findUsers(transaction?: any): Promise, Error>> { + const usersOrError = await this.userRepository.findAll(transaction); + if (usersOrError.isFailure) { + return Result.fail(usersOrError.error); + } + + // Solo devolver usuarios activos + const activeUsers = usersOrError.data.filter((user) => user.isActive); + return Result.ok(new Collection(activeUsers)); + } + + async findUserById(userId: UniqueID, transaction?: any): Promise> { + return await this.userRepository.findById(userId, transaction); + } + + /*public async createUser( + data: { name: string; email: EmailAddress }, + transaction?: Transaction + ): Promise> { + // Evitar duplicados por email + const existingUser = await this.userRepository.findByEmail(data.email); + if (existingUser.isSuccess) { + return Result.fail(new Error("El correo ya está registrado.")); + } + + const newUser = User.create({ + email, + username + }) + return await this.userRepository.save(newUser, transaction); + }*/ +} diff --git a/packages/rdx-auth/src/domain/value-objects/__tests__/hash-password.spec.ts b/packages/rdx-auth/src/domain/value-objects/__tests__/hash-password.spec.ts new file mode 100644 index 0000000..306b646 --- /dev/null +++ b/packages/rdx-auth/src/domain/value-objects/__tests__/hash-password.spec.ts @@ -0,0 +1,44 @@ +import bcrypt from "bcrypt"; +import { HashPassword } from "../hash-password"; + +describe("HashPassword", () => { + test("debe crear una instancia de HashPassword desde un texto plano válido", () => { + const result = HashPassword.createFromPlainText("securepassword"); + expect(result.isSuccess).toBe(true); + expect(result.data).toBeInstanceOf(HashPassword); + }); + + test("debe fallar al crear una instancia con una contraseña demasiado corta", () => { + const result = HashPassword.createFromPlainText("123"); + expect(result.isFailure).toBe(true); + expect(result.error.message).toBe("Password must be at least 6 characters long"); + }); + + test("debe crear una instancia de HashPassword desde un hash válido", () => { + const hashedPassword = bcrypt.hashSync("securepassword", 10); + const result = HashPassword.createFromHash(hashedPassword); + expect(result.isSuccess).toBe(true); + expect(result.data).toBeInstanceOf(HashPassword); + }); + + test("debe verificar correctamente una contraseña válida", async () => { + const password = "securepassword"; + const result = HashPassword.createFromPlainText(password); + expect(result.isSuccess).toBe(true); + const hashPasswordInstance = result.data; + + const isValid = await hashPasswordInstance.verifyPassword(password); + expect(isValid).toBe(true); + }); + + test("debe fallar la verificación con una contraseña incorrecta", async () => { + const password = "securepassword"; + const wrongPassword = "wrongpassword"; + const result = HashPassword.createFromPlainText(password); + expect(result.isSuccess).toBe(true); + const hashPasswordInstance = result.data; + + const isValid = await hashPasswordInstance.verifyPassword(wrongPassword); + expect(isValid).toBe(false); + }); +}); diff --git a/packages/rdx-auth/src/domain/value-objects/auth-user-roles.ts b/packages/rdx-auth/src/domain/value-objects/auth-user-roles.ts new file mode 100644 index 0000000..ce46b0c --- /dev/null +++ b/packages/rdx-auth/src/domain/value-objects/auth-user-roles.ts @@ -0,0 +1,35 @@ +import { ValueObject } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { z } from "zod"; + +const RoleSchema = z.enum(["Admin", "User", "Manager", "Editor"]); + +interface UserRolesProps { + value: string[]; +} + +export class UserRoles extends ValueObject { + static create(roles: string[]): Result { + const result = UserRoles.validate(roles); + + return result.success + ? Result.ok(new UserRoles({ value: result.data })) + : Result.fail(new Error("Invalid user roles")); + } + + private static validate(roles: string[]) { + return z.array(RoleSchema).safeParse(roles); + } + + hasRole(role: string): boolean { + return this.props.value.includes(role); + } + + getValue() { + return this.props.value; + } + + toPrimitive() { + return this.props.value; + } +} diff --git a/packages/rdx-auth/src/domain/value-objects/hash-password.ts b/packages/rdx-auth/src/domain/value-objects/hash-password.ts new file mode 100644 index 0000000..f5a5e95 --- /dev/null +++ b/packages/rdx-auth/src/domain/value-objects/hash-password.ts @@ -0,0 +1,48 @@ +import { ValueObject } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import bcrypt from "bcrypt"; +import { z } from "zod"; + +interface HashPasswordProps { + value: string; +} + +export class HashPassword extends ValueObject { + private static readonly SALT_ROUNDS = 10; + + static createFromPlainText(plainTextPassword: string): Result { + const result = HashPassword.validate(plainTextPassword); + + if (!result.success) { + return Result.fail(new Error(result.error.errors[0].message)); + } + + const hashed = bcrypt.hashSync(result.data, this.SALT_ROUNDS); + return Result.ok(new HashPassword({ value: hashed })); + } + + private static validate(password: string) { + const schema = z.string().min(6, { message: "Password must be at least 6 characters long" }); + return schema.safeParse(password); + } + + static createFromHash(hashedPassword: string): Result { + return Result.ok(new HashPassword({ value: hashedPassword })); + } + + async verifyPassword(plainTextPassword: string): Promise { + return await bcrypt.compare(plainTextPassword, this.props.value); + } + + getValue() { + return this.props.value; + } + + toString() { + return this.props.value; + } + + toPrimitive() { + return this.props.value; + } +} diff --git a/packages/rdx-auth/src/domain/value-objects/index.ts b/packages/rdx-auth/src/domain/value-objects/index.ts new file mode 100644 index 0000000..b3bdb2e --- /dev/null +++ b/packages/rdx-auth/src/domain/value-objects/index.ts @@ -0,0 +1,5 @@ +export * from "./auth-user-roles"; +export * from "./hash-password"; +export * from "./plain-password"; +export * from "./token"; +export * from "./username"; diff --git a/packages/rdx-auth/src/domain/value-objects/plain-password.ts b/packages/rdx-auth/src/domain/value-objects/plain-password.ts new file mode 100644 index 0000000..ce01740 --- /dev/null +++ b/packages/rdx-auth/src/domain/value-objects/plain-password.ts @@ -0,0 +1,36 @@ +import { ValueObject } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { z } from "zod"; + +interface PlainPasswordProps { + value: string; +} + +export class PlainPassword extends ValueObject { + static create(plainTextPassword: string): Result { + const result = PlainPassword.validate(plainTextPassword); + + if (!result.success) { + return Result.fail(new Error(result.error.errors[0].message)); + } + + return Result.ok(new PlainPassword({ value: result.data })); + } + + private static validate(password: string) { + const schema = z.string().min(6, { message: "Password must be at least 6 characters long" }); + return schema.safeParse(password); + } + + getValue() { + return this.props.value; + } + + toString() { + return this.props.value; + } + + toPrimitive() { + return this.props.value; + } +} diff --git a/packages/rdx-auth/src/domain/value-objects/token.ts b/packages/rdx-auth/src/domain/value-objects/token.ts new file mode 100644 index 0000000..1d7b48b --- /dev/null +++ b/packages/rdx-auth/src/domain/value-objects/token.ts @@ -0,0 +1,36 @@ +import { ValueObject } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { z } from "zod"; + +interface TokenProps { + value: string; +} + +export class Token extends ValueObject { + static create(token: string): Result { + const result = Token.validate(token); + + if (!result.success) { + return Result.fail(new Error(result.error.errors[0].message)); + } + + return Result.ok(new Token({ value: result.data })); + } + + private static validate(token: string) { + const schema = z.string().min(1, { message: "Invalid token string" }); + return schema.safeParse(token); + } + + getValue() { + return this.props.value; + } + + toString() { + return this.props.value; + } + + toPrimitive() { + return this.props.value; + } +} diff --git a/packages/rdx-auth/src/domain/value-objects/username.ts b/packages/rdx-auth/src/domain/value-objects/username.ts new file mode 100644 index 0000000..91b7c91 --- /dev/null +++ b/packages/rdx-auth/src/domain/value-objects/username.ts @@ -0,0 +1,41 @@ +import { ValueObject } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { z } from "zod"; + +interface UsernameProps { + value: string; +} + +export class Username extends ValueObject { + static create(username: string): Result { + const result = Username.validate(username); + + return result.success + ? Result.ok(new Username({ value: result.data })) + : Result.fail(new Error(result.error.errors[0].message)); + } + + private static validate(username: string) { + const schema = z + .string() + .min(3, { message: "Username must be at least 3 characters long" }) + .max(30, { message: "Username cannot exceed 30 characters" }) + .regex(/^[a-zA-Z0-9_]+$/, { + message: "Username can only contain letters, numbers, and underscores", + }); + + return schema.safeParse(username); + } + + getValue() { + return this.props.value; + } + + toString() { + return this.props.value; + } + + toPrimitive() { + return this.props.value; + } +} diff --git a/packages/rdx-auth/src/index.ts b/packages/rdx-auth/src/index.ts new file mode 100644 index 0000000..ef47821 --- /dev/null +++ b/packages/rdx-auth/src/index.ts @@ -0,0 +1,4 @@ +export * from "./application"; +export * from "./domain"; +export * from "./presentation"; +export * from "./infraestructure"; diff --git a/packages/rdx-auth/src/infraestructure/express/index.ts b/packages/rdx-auth/src/infraestructure/express/index.ts new file mode 100644 index 0000000..eea524d --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/express/index.ts @@ -0,0 +1 @@ +export * from "./types"; diff --git a/packages/rdx-auth/src/infraestructure/express/types.ts b/packages/rdx-auth/src/infraestructure/express/types.ts new file mode 100644 index 0000000..df2e767 --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/express/types.ts @@ -0,0 +1,10 @@ +import { AuthenticatedUser, TabContext } from "@/domain"; +import { Request } from "express"; + +export interface TabContextRequest extends Request { + tabContext?: TabContext; +} + +export interface AuthenticatedRequest extends Request { + user?: AuthenticatedUser; +} diff --git a/packages/rdx-auth/src/infraestructure/index.ts b/packages/rdx-auth/src/infraestructure/index.ts new file mode 100644 index 0000000..d91838a --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/index.ts @@ -0,0 +1,5 @@ +export * from "./express"; +export * from "./mappers"; +export * from "./middleware"; +export * from "./passport"; +export * from "./sequelize"; diff --git a/packages/rdx-auth/src/infraestructure/mappers/authenticated-user.mapper.ts b/packages/rdx-auth/src/infraestructure/mappers/authenticated-user.mapper.ts new file mode 100644 index 0000000..ccd3d7c --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/mappers/authenticated-user.mapper.ts @@ -0,0 +1,64 @@ +import { AuthenticatedUser, HashPassword, Username } from "@contexts/auth/domain"; +import { EmailAddress, UniqueID } from "core/common/domain"; +import { Result } from "core/common/helpers"; +import { ISequelizeMapper, MapperParamsType, SequelizeMapper } from "core/common/infrastructure"; +import { AuthUserCreationAttributes, AuthUserModel } from "../sequelize"; + +export interface IAuthenticatedUserMapper + extends ISequelizeMapper {} + +export class AuthenticatedUserMapper + extends SequelizeMapper + implements IAuthenticatedUserMapper +{ + public mapToDomain( + source: AuthUserModel, + params?: MapperParamsType + ): Result { + // Crear Value Objects asegurando que sean válidos + const uniqueIdResult = UniqueID.create(source.id); + const usernameResult = Username.create(source.username); + const passwordHashResult = HashPassword.createFromHash(source.hash_password); + const emailResult = EmailAddress.create(source.email); + + // Validar que no haya errores en la creación de los Value Objects + const okOrError = Result.combine([ + uniqueIdResult, + usernameResult, + passwordHashResult, + emailResult, + ]); + if (okOrError.isFailure) { + return Result.fail(okOrError.error.message); + } + + // Crear el agregado de dominio + return AuthenticatedUser.create( + { + username: usernameResult.data!, + email: emailResult.data!, + hashPassword: passwordHashResult.data!, + roles: source.roles || [], + }, + uniqueIdResult.data! + ); + } + + public mapToPersistence( + source: AuthenticatedUser, + params?: MapperParamsType + ): Result { + return Result.ok({ + id: source.id.toString(), + username: source.username.toString(), + email: source.email.toString(), + hash_password: source.hashPassword.toString(), + roles: source.getRoles().map((role) => role.toString()), + //access_token: source.accessToken, + //refresh_token: source.refreshToken, + }); + } +} + +const authenticatedUserMapper: IAuthenticatedUserMapper = new AuthenticatedUserMapper(); +export { authenticatedUserMapper }; diff --git a/packages/rdx-auth/src/infraestructure/mappers/index.ts b/packages/rdx-auth/src/infraestructure/mappers/index.ts new file mode 100644 index 0000000..fcc87a1 --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/mappers/index.ts @@ -0,0 +1,3 @@ +export * from "./authenticated-user.mapper"; +export * from "./tab-context.mapper"; +export * from "./user.mapper"; diff --git a/packages/rdx-auth/src/infraestructure/mappers/tab-context.mapper.ts b/packages/rdx-auth/src/infraestructure/mappers/tab-context.mapper.ts new file mode 100644 index 0000000..b137620 --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/mappers/tab-context.mapper.ts @@ -0,0 +1,62 @@ +import { UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { ISequelizeMapper, MapperParamsType, SequelizeMapper } from "../../../common"; +import { TabContext } from "../../domain"; +import { TabContextCreationAttributes, TabContextModel } from "../sequelize"; + +export interface ITabContextMapper + extends ISequelizeMapper {} + +export class TabContextMapper + extends SequelizeMapper + implements ITabContextMapper +{ + public mapToDomain( + source: TabContextModel, + params?: MapperParamsType + ): Result { + // Crear Value Objects asegurando que sean válidos + const uniqueIdResult = UniqueID.create(source.id); + const tabIdResult = UniqueID.create(source.tab_id); + const userIdResult = UniqueID.create(source.user_id); + //const companyIdResult = UniqueID.create(entity.company_id, false); + //const brachIdResult = UniqueID.create(entity.branch_id, false); + + // Validar que no haya errores en la creación de los Value Objects + const okOrError = Result.combine([ + uniqueIdResult, + tabIdResult, + userIdResult, + //companyIdResult, + //brachIdResult, + ]); + if (okOrError.isFailure) { + return Result.fail(okOrError.error.message); + } + + // Crear el agregado de dominio + return TabContext.create( + { + tabId: tabIdResult.data!, + userId: userIdResult.data!, + //companyId: companyIdResult.data, + //branchId: brachIdResult.data, + }, + uniqueIdResult.data! + ); + } + + public mapToPersistence( + source: TabContext, + params?: MapperParamsType + ): Result { + return Result.ok({ + id: source.id.toString(), + tab_id: source.tabId.toString(), + user_id: source.userId.toString(), + }); + } +} + +const tabContextMapper: ITabContextMapper = new TabContextMapper(); +export { tabContextMapper }; diff --git a/packages/rdx-auth/src/infraestructure/mappers/user.mapper.ts b/packages/rdx-auth/src/infraestructure/mappers/user.mapper.ts new file mode 100644 index 0000000..9f4b59b --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/mappers/user.mapper.ts @@ -0,0 +1,51 @@ +import { EmailAddress, UniqueID } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { ISequelizeMapper, MapperParamsType, SequelizeMapper } from "../../../common"; +import { User, Username } from "../../domain"; +import { UserCreationAttributes, UserModel } from "../sequelize"; + +export interface IUserMapper extends ISequelizeMapper {} + +class UserMapper + extends SequelizeMapper + implements IUserMapper +{ + public mapToDomain(source: UserModel, params?: MapperParamsType): Result { + // Crear Value Objects asegurando que sean válidos + const uniqueIdResult = UniqueID.create(source.id); + const usernameResult = Username.create(source.username); + const emailResult = EmailAddress.create(source.email); + + // Validar que no haya errores en la creación de los Value Objects + const okOrError = Result.combine([uniqueIdResult, usernameResult, emailResult]); + if (okOrError.isFailure) { + return Result.fail(okOrError.error.message); + } + + // Crear el agregado de dominio + return User.create( + { + username: usernameResult.data!, + email: emailResult.data!, + roles: [], + //roles: entity.roles || [], + }, + uniqueIdResult.data! + ); + } + + public mapToPersistence( + source: User, + params?: MapperParamsType + ): Result { + return Result.ok({ + id: source.id.toString(), + username: source.username.toString(), + email: source.email.toString(), + //roles: source.getRoles().map((role) => role.toString()), + }); + } +} + +const userMapper: IUserMapper = new UserMapper(); +export { userMapper }; diff --git a/packages/rdx-auth/src/infraestructure/middleware/index.ts b/packages/rdx-auth/src/infraestructure/middleware/index.ts new file mode 100644 index 0000000..5ce1bf3 --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/middleware/index.ts @@ -0,0 +1,2 @@ +export * from "./passport-auth.middleware"; +export * from "./tab-context.middleware"; diff --git a/packages/rdx-auth/src/infraestructure/middleware/passport-auth.middleware.ts b/packages/rdx-auth/src/infraestructure/middleware/passport-auth.middleware.ts new file mode 100644 index 0000000..229c708 --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/middleware/passport-auth.middleware.ts @@ -0,0 +1,69 @@ +//import { authProvider } from "@contexts/auth/infraestructure"; +import { UniqueID } from "@rdx/ddd-domain"; +import { NextFunction, Response } from "express"; +import { ApiError, ExpressController } from "../../../common"; +import { AuthenticatedRequest } from "../../../common/presentation/express/types"; +import { AuthenticatedUser } from "../../domain"; +import { authProvider } from "../passport"; + +// Comprueba el rol del usuario +const _authorizeUser = (condition: (user: AuthenticatedUser) => boolean) => { + return (req: AuthenticatedRequest, res: Response, next: NextFunction) => { + const user = req.user as AuthenticatedUser; + + if (!user || !condition(user)) { + return ExpressController.errorResponse( + new ApiError({ + status: 401, + title: "Unauthorized", + detail: "You are not authorized to access this resource.", + }), + res + ); + } + + return next(); + }; +}; + +// Verifica que el usuario esté autenticado +export const checkUser = [ + authProvider.authenticateJWT(), + _authorizeUser((user) => true /*user.isUser*/), +]; + +// Verifica que el usuario sea administrador +export const checkUserIsAdmin = [ + authProvider.authenticateJWT(), + _authorizeUser((user) => true /*user.isAdmin*/), +]; + +// Middleware para verificar que el usuario sea administrador o el dueño de los datos (self) +export const checkUserIsAdminOrOwner = [ + (req: AuthenticatedRequest, res: Response, next: NextFunction) => { + const user = req.user as AuthenticatedUser; + const { userId } = req.params; + + // Si el usuario es admin, está autorizado + if (user.isAdmin) { + return next(); + } + + // Si el usuario es sí mismo + if (user.isUser && userId) { + const paramIdOrError = UniqueID.create(userId); + if (paramIdOrError.isSuccess && user.id.equals(paramIdOrError.data)) { + return next(); + } + } + + return ExpressController.errorResponse( + new ApiError({ + status: 401, + title: "Unauthorized", + detail: "You are not authorized to access this resource.", + }), + res + ); + }, +]; diff --git a/packages/rdx-auth/src/infraestructure/middleware/tab-context.middleware.ts b/packages/rdx-auth/src/infraestructure/middleware/tab-context.middleware.ts new file mode 100644 index 0000000..9de9cfe --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/middleware/tab-context.middleware.ts @@ -0,0 +1,3 @@ +import { authProvider } from "../passport"; + +export const checkTabContext = [authProvider.authenticateTabId()]; diff --git a/packages/rdx-auth/src/infraestructure/passport/index.ts b/packages/rdx-auth/src/infraestructure/passport/index.ts new file mode 100644 index 0000000..88929bc --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/passport/index.ts @@ -0,0 +1,15 @@ +import { AuthService, TabContextService } from "@/domain"; +import { authenticatedUserRepository } from "@/infraestructure"; +import { SequelizeTransactionManager } from "@rdx/core"; +import { Sequelize } from "sequelize"; +import { tabContextRepository } from "../sequelize"; +import { PassportAuthProvider } from "./passport-auth-provider"; + +export const buildPassportController = (database: Sequelize) => { + const transactionManager = new SequelizeTransactionManager(database); + + const authService = new AuthService(authenticatedUserRepository, tabContextRepository); + const tabContextService = new TabContextService(tabContextRepository); + + return new PassportAuthProvider(authService, tabContextService, transactionManager); +}; diff --git a/packages/rdx-auth/src/infraestructure/passport/jwt.helper.ts b/packages/rdx-auth/src/infraestructure/passport/jwt.helper.ts new file mode 100644 index 0000000..c0f3981 --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/passport/jwt.helper.ts @@ -0,0 +1,14 @@ +import jwt from "jsonwebtoken"; + +const SECRET_KEY: jwt.Secret = process.env.JWT_SECRET || "supersecretkey"; + +export class JwtHelper { + static generateToken(payload: object, expiresIn = "1h"): string { + const params: jwt.SignOptions = { expiresIn: expiresIn as jwt.SignOptions["expiresIn"] }; + return jwt.sign(payload, SECRET_KEY, params); + } + + static verifyToken(token: string): any { + return jwt.verify(token, SECRET_KEY); + } +} diff --git a/packages/rdx-auth/src/infraestructure/passport/passport-auth-provider.ts b/packages/rdx-auth/src/infraestructure/passport/passport-auth-provider.ts new file mode 100644 index 0000000..aae9e60 --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/passport/passport-auth-provider.ts @@ -0,0 +1,115 @@ +import { NextFunction, Response } from "express"; + +import { TabContext } from "@contexts/auth/domain"; +import { IAuthService, ITabContextService } from "@contexts/auth/domain/services"; +import { EmailAddress, UniqueID } from "core/common/domain"; +import { Result } from "core/common/helpers"; +import { ITransactionManager } from "core/common/infrastructure/database"; +import { logger } from "core/common/infrastructure/logger"; +import passport from "passport"; +import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt"; +import { TabContextRequest } from "../../../../core/presentation/express/types"; + +const SECRET_KEY = process.env.JWT_SECRET || "supersecretkey"; + +export class PassportAuthProvider { + private async _getContextByTabId(value: string): Promise> { + const tabIdOrError = UniqueID.create(value, false); + if (tabIdOrError.isFailure) { + return Result.fail(new Error("Invalid tab ID")); + } + + const tabResult = await this.tabContextService.getContextByTabId(tabIdOrError.data); + if (tabResult.isFailure) { + return Result.fail(new Error("Invalid token data")); + } + + return Result.ok(tabResult.data); + } + + private async _getUserByToken(tokenPayload: any) { + const { user_id, email, roles } = tokenPayload; + + const userIdVO = UniqueID.create(user_id); + const emailVO = EmailAddress.create(email!); + + const okOrError = Result.combine([userIdVO, emailVO]); + if (okOrError.isFailure) { + return Result.fail(okOrError.error.message); + } + + const userResult = await this.authService.getUserByEmail(emailVO.data); + + if (userResult.isFailure) { + return Result.fail(new Error("Invalid token data")); + } + + const user = userResult.data; + + const checkUserId = user.id.equals(userIdVO.data); + const checkRoles = true; //user.hasRoles(roles); + + if (!checkUserId || !checkRoles) { + return Result.fail(new Error("Invalid token data")); + } + + return Result.ok(user); + } + + constructor( + private readonly authService: IAuthService, + private readonly tabContextService: ITabContextService, + private readonly transactionManager: ITransactionManager + ) {} + + /** + * 🔹 Configura PassportJS + */ + initialize(): void { + const jwtOptions = { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: SECRET_KEY, + }; + + passport.use( + "jwt", + new JwtStrategy(jwtOptions, async (tokenPayload, done) => { + try { + const userOrError = await this._getUserByToken(tokenPayload); + if (userOrError.isFailure) { + return done(userOrError.error, false, { message: "Invalid JWT data" }); + } + + return done(null, userOrError.data); + } catch (error) { + return done(error, false); + } + }) + ); + + passport.initialize(); + } + + authenticateJWT() { + logger.debug("Authenticating JWT"); + return passport.authenticate("jwt", { session: false }); + } + + authenticateTabId() { + logger.debug("Authenticating Tab ID"); + return async (req: TabContextRequest, res: Response, next: NextFunction) => { + const tabIdValue = req.header("X-Tab-ID"); + if (!tabIdValue) { + return res.status(401).json({ message: "Tab ID is required" }); + } + + const tabContextOrError = await this._getContextByTabId(tabIdValue); + if (tabContextOrError.isFailure) { + return res.status(401).json({ message: "Invalid tab context data" }); + } + + req.tabContext = tabContextOrError.data; + return next(); + }; + } +} diff --git a/packages/rdx-auth/src/infraestructure/passport/passport.ts b/packages/rdx-auth/src/infraestructure/passport/passport.ts new file mode 100644 index 0000000..910be66 --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/passport/passport.ts @@ -0,0 +1,26 @@ +import passport from "passport"; +import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt"; + +const SECRET_KEY = process.env.JWT_SECRET || "supersecretkey"; + +// Configuración de la estrategia JWT +const jwtOptions = { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: SECRET_KEY, +}; + +passport.use( + new JwtStrategy(jwtOptions, async (payload, done) => { + try { + const userResult = await authenticatedUserRepository.findById(payload.userId); + if (userResult.isFailure) { + return done(null, false); + } + return done(null, userResult.data); + } catch (error) { + return done(error, false); + } + }) +); + +export default passport; diff --git a/packages/rdx-auth/src/infraestructure/sequelize/auth-user.model.ts b/packages/rdx-auth/src/infraestructure/sequelize/auth-user.model.ts new file mode 100644 index 0000000..8c4c0ee --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/sequelize/auth-user.model.ts @@ -0,0 +1,74 @@ +import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize"; + +export type AuthUserCreationAttributes = InferCreationAttributes; + +export class AuthUserModel extends Model< + InferAttributes, + InferCreationAttributes +> { + // To avoid table creation + /*static async sync(): Promise { + return Promise.resolve(); + }*/ + + declare id: string; + declare username: string; + declare email: string; + declare hash_password: string; + declare roles: string[]; +} + +export default (sequelize: Sequelize) => { + AuthUserModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + }, + username: { + type: DataTypes.STRING, + allowNull: false, + }, + email: { + type: DataTypes.STRING, + allowNull: false, + }, + hash_password: { + type: DataTypes.STRING, + allowNull: false, + }, + roles: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: "USER", + get(this: AuthUserModel): string[] { + const rawValue = this.getDataValue("roles") as any; + return String(rawValue).split(";"); + }, + set(this: AuthUserModel, value: string[]) { + const rawValue = value.join(";") as any; + this.setDataValue("roles", rawValue); + }, + }, + }, + { + sequelize, + tableName: "users", + paranoid: true, // softs deletes + timestamps: true, + + createdAt: "created_at", + updatedAt: "updated_at", + deletedAt: "deleted_at", + + indexes: [{ name: "email_idx", fields: ["email"], unique: true }], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + + defaultScope: {}, + + scopes: {}, + } + ); + return AuthUserModel; +}; diff --git a/packages/rdx-auth/src/infraestructure/sequelize/authenticated-user.repository.ts b/packages/rdx-auth/src/infraestructure/sequelize/authenticated-user.repository.ts new file mode 100644 index 0000000..0e7576f --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/sequelize/authenticated-user.repository.ts @@ -0,0 +1,95 @@ +import { SequelizeRepository } from "@rdx/core"; +import { EmailAddress } from "@rdx/ddd-domain"; +import { Result } from "@rdx/utils"; +import { Transaction } from "sequelize"; +import { AuthenticatedUser, IAuthenticatedUserRepository, Username } from "../../domain"; +import { authenticatedUserMapper, IAuthenticatedUserMapper } from "../mappers"; +import { AuthUserModel } from "./auth-user.model"; + +class AuthenticatedUserRepository + extends SequelizeRepository + implements IAuthenticatedUserRepository +{ + private readonly _mapper!: IAuthenticatedUserMapper; + + /** + * 🔹 Función personalizada para mapear errores de unicidad en autenticación + */ + private _customErrorMapper(error: Error): string | null { + if (error.name === "SequelizeUniqueConstraintError") { + return "User with this email or username already exists"; + } + + return null; + } + + constructor(mapper: IAuthenticatedUserMapper) { + super(); + this._mapper = mapper; + } + + async userExists( + username: Username, + email: EmailAddress, + transaction?: Transaction + ): Promise> { + try { + const userWithEmail = await this._findById( + AuthUserModel, + "email", + email.toString(), + transaction + ); + + const userWithUsername = await this._findById( + AuthUserModel, + "username", + username.toString(), + transaction + ); + + return Result.ok(Boolean(userWithEmail || userWithUsername)); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async getUserByEmail( + email: EmailAddress, + transaction?: Transaction + ): Promise> { + try { + const rawUser: any = await this._getBy( + AuthUserModel, + "email", + email.toString(), + {}, + transaction + ); + + if (!rawUser === true) { + return Result.fail(new Error("User with email not exists")); + } + + return this._mapper.mapToDomain(rawUser); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async createUser( + user: AuthenticatedUser, + transaction?: Transaction + ): Promise> { + try { + const persistenceData = this._mapper.mapToPersistence(user); + await AuthUserModel.create(persistenceData.data, { transaction }); + return Result.ok(); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } +} + +const authenticatedUserRepository = new AuthenticatedUserRepository(authenticatedUserMapper); +export { authenticatedUserRepository }; diff --git a/packages/rdx-auth/src/infraestructure/sequelize/index.ts b/packages/rdx-auth/src/infraestructure/sequelize/index.ts new file mode 100644 index 0000000..e3fc619 --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/sequelize/index.ts @@ -0,0 +1,25 @@ +import { IAuthenticatedUserRepository, ITabContextRepository, IUserRepository } from "@/domain"; +import { authenticatedUserRepository } from "./authenticated-user.repository"; +import { tabContextRepository } from "./tab-context.repository"; +import { userRepository } from "./user.repository"; + +export * from "./auth-user.model"; +export * from "./authenticated-user.repository"; + +export * from "./tab-context.model"; +export * from "./tab-context.repository"; + +export * from "./user.model"; +export * from "./user.repository"; + +export const createAuthenticatedUserRepository = (): IAuthenticatedUserRepository => { + return authenticatedUserRepository; +}; + +export const createTabContextRepository = (): ITabContextRepository => { + return tabContextRepository; +}; + +export const createUserRepository = (): IUserRepository => { + return userRepository; +}; diff --git a/packages/rdx-auth/src/infraestructure/sequelize/tab-context.model.ts b/packages/rdx-auth/src/infraestructure/sequelize/tab-context.model.ts new file mode 100644 index 0000000..c6e9dd4 --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/sequelize/tab-context.model.ts @@ -0,0 +1,58 @@ +import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize"; + +export type TabContextCreationAttributes = InferCreationAttributes; + +export class TabContextModel extends Model< + InferAttributes, + InferCreationAttributes +> { + // To avoid table creation + /*static async sync(): Promise { + return Promise.resolve(); + }*/ + static associate(connection: Sequelize) { + const { AuthUserModel } = connection.models; + } + + declare id: string; + declare tab_id: string; + declare user_id: string; +} + +export default (sequelize: Sequelize) => { + TabContextModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + }, + user_id: { + type: DataTypes.UUID, + allowNull: false, + }, + tab_id: { + type: DataTypes.UUID, + allowNull: false, + }, + }, + { + sequelize, + tableName: "user_tab_contexts", + paranoid: true, // softs deletes + timestamps: true, + + createdAt: "created_at", + updatedAt: "updated_at", + deletedAt: "deleted_at", + + indexes: [{ name: "tab_id_idx", fields: ["tab_id"], unique: true }], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + + defaultScope: {}, + + scopes: {}, + } + ); + return TabContextModel; +}; diff --git a/packages/rdx-auth/src/infraestructure/sequelize/tab-context.repository.ts b/packages/rdx-auth/src/infraestructure/sequelize/tab-context.repository.ts new file mode 100644 index 0000000..722102c --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/sequelize/tab-context.repository.ts @@ -0,0 +1,130 @@ +import { ITabContextRepository, TabContext } from "@contexts/auth/domain/"; +import { UniqueID } from "core/common/domain"; +import { Result } from "core/common/helpers"; +import { SequelizeRepository } from "core/common/infrastructure"; +import { Op, Transaction } from "sequelize"; +import { ITabContextMapper, tabContextMapper } from "../mappers"; +import { TabContextModel } from "./tab-context.model"; + +class TabContextRepository + extends SequelizeRepository + implements ITabContextRepository +{ + private readonly _mapper!: ITabContextMapper; + + /** + * 🔹 Función personalizada para mapear errores de unicidad en autenticación + */ + private _customErrorMapper(error: Error): string | null { + if (error.name === "SequelizeUniqueConstraintError") { + return "Tab context already exists"; + } + + return null; + } + + constructor(mapper: ITabContextMapper) { + super(); + this._mapper = mapper; + } + + async getContextByTabId( + tabId: UniqueID, + transaction?: Transaction + ): Promise> { + try { + const rawContext = await this._getBy( + TabContextModel, + "tab_id", + tabId.toString(), + {}, + transaction + ); + + if (!rawContext === true) { + return Result.fail(new Error("Tab context not exists")); + } + + return this._mapper.mapToDomain(rawContext); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async contextExistsByTabId(tabId: UniqueID, transaction?: any): Promise> { + try { + const result: any = await this._exists( + TabContextModel, + "tab_id", + tabId.toString(), + transaction + ); + + return Result.ok(Boolean(result)); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + /** + * Registra un contexto para un tab id o actualiza si ya existe + * @param context + * @param transaction + * @returns + */ + + async registerContextByTabId( + context: TabContext, + transaction?: Transaction + ): Promise> { + try { + const { userId, tabId } = context; + const persistenceData = this._mapper.mapToPersistence(context); + + // Si existe el contexto de ese tabId, lo actualizo. + + if (await this._exists(TabContextModel, "tab_id", tabId.toString())) { + await TabContextModel.update(persistenceData.data, { + where: { [Op.and]: [{ tab_id: tabId.toString() }, { user_id: userId.toString() }] }, + transaction, + }); + } else { + await TabContextModel.create(persistenceData.data, { + include: [{ all: true }], + transaction, + }); + } + + return Result.ok(); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + /** + * Desregistra un contexto para un tab id o actualiza si ya existe + * @param context + * @param transaction + * @returns + */ + + async unregisterContextByTabId( + context: TabContext, + transaction?: Transaction + ): Promise> { + try { + const { userId, tabId } = context; + + await TabContextModel.destroy({ + where: { [Op.and]: [{ tab_id: tabId.toString() }, { user_id: userId.toString() }] }, + transaction, + }); + return Result.ok(); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } +} + +const tabContextRepository = new TabContextRepository(tabContextMapper); +export { tabContextRepository }; diff --git a/packages/rdx-auth/src/infraestructure/sequelize/user.model.ts b/packages/rdx-auth/src/infraestructure/sequelize/user.model.ts new file mode 100644 index 0000000..a15e58a --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/sequelize/user.model.ts @@ -0,0 +1,55 @@ +import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize"; + +export type UserCreationAttributes = InferCreationAttributes; + +export class UserModel extends Model< + InferAttributes, + InferCreationAttributes +> { + // To avoid table creation + /*static async sync(): Promise { + return Promise.resolve(); + }*/ + + declare id: string; + declare username: string; + declare email: string; +} + +export default (sequelize: Sequelize) => { + UserModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + }, + username: { + type: DataTypes.STRING, + allowNull: false, + }, + email: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + sequelize, + tableName: "users", + paranoid: true, // softs deletes + timestamps: true, + + createdAt: "created_at", + updatedAt: "updated_at", + deletedAt: "deleted_at", + + indexes: [{ name: "email_idx", fields: ["email"], unique: true }], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + + defaultScope: {}, + + scopes: {}, + } + ); + return UserModel; +}; diff --git a/packages/rdx-auth/src/infraestructure/sequelize/user.repository.ts b/packages/rdx-auth/src/infraestructure/sequelize/user.repository.ts new file mode 100644 index 0000000..8a685d3 --- /dev/null +++ b/packages/rdx-auth/src/infraestructure/sequelize/user.repository.ts @@ -0,0 +1,72 @@ +import { IUserRepository, User } from "@/domain"; +import { SequelizeRepository } from "@rdx/core"; +import { EmailAddress, UniqueID } from "@rdx/ddd-domain"; +import { Collection, Result } from "@rdx/utils"; +import { Transaction } from "sequelize"; +import { IUserMapper, userMapper } from "../mappers"; +import { UserModel } from "./user.model"; + +class UserRepository extends SequelizeRepository implements IUserRepository { + private readonly _mapper!: IUserMapper; + + /** + * 🔹 Función personalizada para mapear errores de unicidad en autenticación + */ + private _customErrorMapper(error: Error): string | null { + if (error.name === "SequelizeUniqueConstraintError") { + return "User with this email or username already exists"; + } + + return null; + } + + constructor(mapper: IUserMapper) { + super(); + this._mapper = mapper; + } + + async findAll(transaction?: Transaction): Promise, Error>> { + try { + const rawUsers: any = await this._findAll(UserModel, {}, transaction); + + if (!rawUsers === true) { + return Result.fail(new Error("User with email not exists")); + } + + return this._mapper.mapArrayToDomain(rawUsers); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async findById(id: UniqueID, transaction?: Transaction): Promise> { + try { + const rawUser: any = await this._getById(UserModel, id, {}, transaction); + + if (!rawUser === true) { + return Result.fail(new Error(`User with id ${id.toString()} not exists`)); + } + + return this._mapper.mapToDomain(rawUser); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async findByEmail(email: EmailAddress, transaction?: Transaction): Promise> { + try { + const rawUser: any = await this._getBy(UserModel, "email", email.toString(), {}, transaction); + + if (!rawUser === true) { + return Result.fail(new Error(`User with email ${email.toString()} not exists`)); + } + + return this._mapper.mapToDomain(rawUser); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } +} + +const userRepository = new UserRepository(userMapper); +export { userRepository }; diff --git a/packages/rdx-auth/src/presentation/controllers/index.ts b/packages/rdx-auth/src/presentation/controllers/index.ts new file mode 100644 index 0000000..644a8bb --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/index.ts @@ -0,0 +1,5 @@ +export * from "./listUsers"; +export * from "./login"; +export * from "./logout"; +export * from "./refreshToken"; +export * from "./register"; diff --git a/packages/rdx-auth/src/presentation/controllers/listUsers/index.ts b/packages/rdx-auth/src/presentation/controllers/listUsers/index.ts new file mode 100644 index 0000000..770ee76 --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/listUsers/index.ts @@ -0,0 +1,17 @@ +import { ListUsersUseCase } from "@/application"; +import { UserService } from "@/domain"; +import { userRepository } from "@/infraestructure"; +import { SequelizeTransactionManager } from "@rdx/core"; +import { Sequelize } from "sequelize"; +import { ListUsersController } from "./list-users.controller"; +import { listUsersPresenter } from "./list-users.presenter"; + +export const buildListUsersController = (database: Sequelize) => { + const transactionManager = new SequelizeTransactionManager(database); + const userService = new UserService(userRepository); + + const useCase = new ListUsersUseCase(userService, transactionManager); + const presenter = listUsersPresenter; + + return new ListUsersController(useCase, presenter); +}; diff --git a/packages/rdx-auth/src/presentation/controllers/listUsers/list-users.controller.ts b/packages/rdx-auth/src/presentation/controllers/listUsers/list-users.controller.ts new file mode 100644 index 0000000..cf9449c --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/listUsers/list-users.controller.ts @@ -0,0 +1,37 @@ +import { ListUsersUseCase } from "@/application"; +import { IListUsersPresenter } from "@/presentation/controllers/listUsers/list-users.presenter"; +import { ExpressController } from "@rdx/core"; + +export class ListUsersController extends ExpressController { + public constructor( + private readonly listUsers: ListUsersUseCase, + private readonly presenter: IListUsersPresenter + ) { + super(); + } + + protected async executeImpl() { + const usersOrError = await this.listUsers.execute(); + + if (usersOrError.isFailure) { + return this.handleError(usersOrError.error); + } + + return this.ok(this.presenter.toDTO(usersOrError.data)); + } + + private handleError(error: Error) { + const message = error.message; + + if ( + message.includes("Database connection lost") || + message.includes("Database request timed out") + ) { + return this.unavailableError( + "Database service is currently unavailable. Please try again later." + ); + } + + return this.conflictError(message); + } +} diff --git a/packages/rdx-auth/src/presentation/controllers/listUsers/list-users.presenter.ts b/packages/rdx-auth/src/presentation/controllers/listUsers/list-users.presenter.ts new file mode 100644 index 0000000..22f6212 --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/listUsers/list-users.presenter.ts @@ -0,0 +1,16 @@ +import { Collection, ensureString } from "@rdx/utils"; +import { User } from "../../../domain"; +import { IListUsersResponseDTO } from "../../dto"; + +export interface IListUsersPresenter { + toDTO: (users: Collection) => IListUsersResponseDTO[]; +} + +export const listUsersPresenter: IListUsersPresenter = { + toDTO: (users: Collection): IListUsersResponseDTO[] => + users.map((user) => ({ + id: ensureString(user.id.toString()), + email: ensureString(user.email.toString()), + username: ensureString(user.username.toString()), + })), +}; diff --git a/packages/rdx-auth/src/presentation/controllers/login/index.ts b/packages/rdx-auth/src/presentation/controllers/login/index.ts new file mode 100644 index 0000000..f3d9e35 --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/login/index.ts @@ -0,0 +1,17 @@ +import { LoginUseCase } from "@/application"; +import { AuthService } from "@/domain"; +import { authenticatedUserRepository, tabContextRepository } from "@/infraestructure"; +import { SequelizeTransactionManager } from "@rdx/core"; +import { Sequelize } from "sequelize"; +import { LoginController } from "./login.controller"; +import { loginPresenter } from "./login.presenter"; + +export const buildLoginController = (database: Sequelize) => { + const transactionManager = new SequelizeTransactionManager(database); + const authService = new AuthService(authenticatedUserRepository, tabContextRepository); + + const useCase = new LoginUseCase(authService, transactionManager); + const presenter = loginPresenter; + + return new LoginController(useCase, presenter); +}; diff --git a/packages/rdx-auth/src/presentation/controllers/login/login.controller.ts b/packages/rdx-auth/src/presentation/controllers/login/login.controller.ts new file mode 100644 index 0000000..944f12b --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/login/login.controller.ts @@ -0,0 +1,48 @@ +import { LoginUseCase } from "@/application"; +import { LoginData } from "@/domain"; +import { ILoginPresenter } from "@/presentation/controllers/login/login.presenter"; +import { ExpressController } from "@rdx/core"; + +export class LoginController extends ExpressController { + public constructor( + private readonly login: LoginUseCase, + private readonly presenter: ILoginPresenter + ) { + super(); + } + + async executeImpl() { + const loginDataOrError = LoginData.createFromPrimitives({ + email: this.req.body.email, + plainPassword: this.req.body.password, + tabId: String(this.req.headers["x-tab-id"]), + }); + + if (loginDataOrError.isFailure) { + return this.clientError("Invalid input data", loginDataOrError.error); + } + + const loginResultOrError = await this.login.execute(loginDataOrError.data); + + if (loginResultOrError.isFailure) { + return this.handleError(loginResultOrError.error); + } + + return this.ok(this.presenter.toDTO(loginResultOrError.data)); + } + + private handleError(error: Error) { + const message = error.message; + + if ( + message.includes("Database connection lost") || + message.includes("Database request timed out") + ) { + return this.unavailableError( + "Database service is currently unavailable. Please try again later." + ); + } + + return this.unauthorizedError(message); + } +} diff --git a/packages/rdx-auth/src/presentation/controllers/login/login.presenter.ts b/packages/rdx-auth/src/presentation/controllers/login/login.presenter.ts new file mode 100644 index 0000000..2888ff1 --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/login/login.presenter.ts @@ -0,0 +1,56 @@ +import { AuthenticatedUser, TabContext, Token } from "../../../domain"; +import { ILoginUserResponseDTO } from "../../dto"; + +export interface ILoginPresenter { + toDTO: (data: { + user: AuthenticatedUser; + tabContext: TabContext; + tokens: { + accessToken: Token; + refreshToken: Token; + }; + }) => ILoginUserResponseDTO; +} + +export const loginPresenter: ILoginPresenter = { + toDTO: (data: { + user: AuthenticatedUser; + tabContext: TabContext; + tokens: { + accessToken: Token; + refreshToken: Token; + }; + }): ILoginUserResponseDTO => { + const { + user, + tabContext, + tokens: { accessToken, refreshToken }, + } = data; + + const userData = { + id: user.id.toString(), + username: user.username.toString(), + email: user.email.toString(), + //roles: user.getRoles().map((role) => role.toString()), + }; + + const tabContextData = { + id: tabContext.id.toString(), + tab_id: tabContext.tabId.toString(), + user_id: tabContext.userId.toString(), + }; + + return { + user: { + id: userData.id, + email: userData.email, + username: userData.username, + tab_id: tabContextData.tab_id, + }, + tokens: { + access_token: accessToken.toString(), + refresh_token: refreshToken.toString(), + }, + }; + }, +}; diff --git a/packages/rdx-auth/src/presentation/controllers/logout/index.ts b/packages/rdx-auth/src/presentation/controllers/logout/index.ts new file mode 100644 index 0000000..9547d6c --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/logout/index.ts @@ -0,0 +1,16 @@ +import { AuthService } from "@/domain"; +import { authenticatedUserRepository, tabContextRepository } from "@/infraestructure"; +import { SequelizeTransactionManager } from "@rdx/core"; + +import { LogoutUseCase } from "@/application"; +import { Sequelize } from "sequelize"; +import { LogoutController } from "./logout.controller"; + +export const buildLogoutController = (database: Sequelize) => { + const transactionManager = new SequelizeTransactionManager(database); + const authService = new AuthService(authenticatedUserRepository, tabContextRepository); + + const useCase = new LogoutUseCase(authService, transactionManager); + + return new LogoutController(useCase); +}; diff --git a/packages/rdx-auth/src/presentation/controllers/logout/logout.controller.ts b/packages/rdx-auth/src/presentation/controllers/logout/logout.controller.ts new file mode 100644 index 0000000..303807c --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/logout/logout.controller.ts @@ -0,0 +1,49 @@ +import { LogoutUseCase } from "@/application"; +import { AuthenticatedUser, LogoutData, TabContext } from "@/domain"; +import { TabContextRequest } from "@/infraestructure"; +import { ExpressController } from "@rdx/core"; + +export class LogoutController extends ExpressController { + public constructor(private readonly logout: LogoutUseCase) { + super(); + } + + async executeImpl() { + const user = this.req.user as AuthenticatedUser; + const tabContext = (this.req as TabContextRequest).tabContext as TabContext; + + const logoutDataOrError = LogoutData.create({ + email: user.email, + tabId: tabContext.tabId, + }); + + if (logoutDataOrError.isFailure) { + return this.clientError("Invalid input data", logoutDataOrError.error); + } + + const logoutOrError = await this.logout.execute(logoutDataOrError.data); + + if (logoutOrError.isFailure) { + return this.handleError(logoutOrError.error); + } + + // Habría que invalidar el token del cliente + + return this.ok(); + } + + private handleError(error: Error) { + const message = error.message; + + if ( + message.includes("Database connection lost") || + message.includes("Database request timed out") + ) { + return this.unavailableError( + "Database service is currently unavailable. Please try again later." + ); + } + + return this.clientError(message); + } +} diff --git a/packages/rdx-auth/src/presentation/controllers/refreshToken/index.ts b/packages/rdx-auth/src/presentation/controllers/refreshToken/index.ts new file mode 100644 index 0000000..77d8be1 --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/refreshToken/index.ts @@ -0,0 +1,18 @@ +import { RefreshTokenUseCase } from "@/application"; +import { AuthService } from "@/domain"; +import { authenticatedUserRepository, tabContextRepository } from "@/infraestructure"; +import { SequelizeTransactionManager } from "@rdx/core"; + +import { Sequelize } from "sequelize"; +import { RefreshTokenController } from "./refresh-token.controller"; +import { refreshTokenPresenter } from "./refresh-token.presenter"; + +export const buildRefreshTokenController = (database: Sequelize) => { + const transactionManager = new SequelizeTransactionManager(database); + const authService = new AuthService(authenticatedUserRepository, tabContextRepository); + + const useCase = new RefreshTokenUseCase(authService, transactionManager); + const presenter = refreshTokenPresenter; + + return new RefreshTokenController(useCase, presenter); +}; diff --git a/packages/rdx-auth/src/presentation/controllers/refreshToken/refresh-token.controller.ts b/packages/rdx-auth/src/presentation/controllers/refreshToken/refresh-token.controller.ts new file mode 100644 index 0000000..803692f --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/refreshToken/refresh-token.controller.ts @@ -0,0 +1,45 @@ +import { RefreshTokenUseCase } from "@/application"; +import { Token } from "@/domain"; +import { ExpressController } from "@rdx/core"; +import { IRefreshTokenPresenter } from "./refresh-token.presenter"; + +export class RefreshTokenController extends ExpressController { + public constructor( + private readonly refreshToken: RefreshTokenUseCase, + private readonly presenter: IRefreshTokenPresenter + ) { + super(); + } + + async executeImpl() { + //const tabId = String(this.req.headers["x-tab-id"]); + const refreshTokenOrError = Token.create(String(this.req.body.refresh_token)); + + if (refreshTokenOrError.isFailure) { + return this.clientError("Invalid input data", refreshTokenOrError.error); + } + + const newRefreshTokenOrError = await this.refreshToken.execute(refreshTokenOrError.data); + + if (newRefreshTokenOrError.isFailure) { + return this.handleError(newRefreshTokenOrError.error); + } + + return this.created(this.presenter.toDto(newRefreshTokenOrError.data)); + } + + private handleError(error: Error) { + const message = error.message; + + if ( + message.includes("Database connection lost") || + message.includes("Database request timed out") + ) { + return this.unavailableError( + "Database service is currently unavailable. Please try again later." + ); + } + + return this.internalServerError(message); + } +} diff --git a/packages/rdx-auth/src/presentation/controllers/refreshToken/refresh-token.presenter.ts b/packages/rdx-auth/src/presentation/controllers/refreshToken/refresh-token.presenter.ts new file mode 100644 index 0000000..d21bd9b --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/refreshToken/refresh-token.presenter.ts @@ -0,0 +1,14 @@ +import { Token } from "@/domain"; +import { IRefreshTokenResponseDTO } from "@/presentation/dto"; + +export interface IRefreshTokenPresenter { + toDto: (refreshToken: Token) => IRefreshTokenResponseDTO; +} + +export const refreshTokenPresenter: IRefreshTokenPresenter = { + toDto: (refreshToken: Token): IRefreshTokenResponseDTO => { + return { + refresh_token: refreshToken.toString(), + }; + }, +}; diff --git a/packages/rdx-auth/src/presentation/controllers/register/index.ts b/packages/rdx-auth/src/presentation/controllers/register/index.ts new file mode 100644 index 0000000..db67c06 --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/register/index.ts @@ -0,0 +1,18 @@ +import { RegisterUseCase } from "@/application"; +import { AuthService } from "@/domain"; +import { SequelizeTransactionManager } from "@rdx/core"; + +import { authenticatedUserRepository, tabContextRepository } from "@/infraestructure"; +import { Sequelize } from "sequelize"; +import { RegisterController } from "./register.controller"; +import { registerPresenter } from "./register.presenter"; + +export const buildRegisterController = (database: Sequelize) => { + const transactionManager = new SequelizeTransactionManager(database); + const authService = new AuthService(authenticatedUserRepository, tabContextRepository); + + const useCase = new RegisterUseCase(authService, transactionManager); + const presenter = registerPresenter; + + return new RegisterController(useCase, presenter); +}; diff --git a/packages/rdx-auth/src/presentation/controllers/register/register.controller.ts b/packages/rdx-auth/src/presentation/controllers/register/register.controller.ts new file mode 100644 index 0000000..3c639d9 --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/register/register.controller.ts @@ -0,0 +1,52 @@ +import { RegisterUseCase } from "@/application"; +import { RegisterData } from "@/domain"; +import { ExpressController } from "@rdx/core"; +import { IRegisterPresenter } from "./register.presenter"; + +export class RegisterController extends ExpressController { + public constructor( + private readonly register: RegisterUseCase, + private readonly presenter: IRegisterPresenter + ) { + super(); + } + + async executeImpl() { + const registerDataOrError = RegisterData.createFromPrimitives({ + email: this.req.body.email, + username: this.req.body.username, + plainPassword: this.req.body.password, + }); + + if (registerDataOrError.isFailure) { + return this.clientError("Invalid input data", registerDataOrError.error); + } + + const userOrError = await this.register.execute(registerDataOrError.data); + + if (userOrError.isFailure) { + return this.handleError(userOrError.error); + } + + return this.created(this.presenter.toDto(userOrError.data)); + } + + private handleError(error: Error) { + const message = error.message; + + if (message.includes("User with this email or username already exists")) { + return this.conflictError(message); + } + + if ( + message.includes("Database connection lost") || + message.includes("Database request timed out") + ) { + return this.unavailableError( + "Database service is currently unavailable. Please try again later." + ); + } + + return this.internalServerError(message); + } +} diff --git a/packages/rdx-auth/src/presentation/controllers/register/register.presenter.ts b/packages/rdx-auth/src/presentation/controllers/register/register.presenter.ts new file mode 100644 index 0000000..a296fd0 --- /dev/null +++ b/packages/rdx-auth/src/presentation/controllers/register/register.presenter.ts @@ -0,0 +1,21 @@ +import { AuthenticatedUser } from "@/domain"; +import { IRegisterUserResponseDTO } from "@/presentation/dto"; + +export interface IRegisterPresenter { + toDto: (user: AuthenticatedUser) => IRegisterUserResponseDTO; +} + +export const registerPresenter: IRegisterPresenter = { + toDto: (user: AuthenticatedUser): IRegisterUserResponseDTO => { + //const { user, token, refreshToken } = loginUser; + //const roles = user.getRoles()?.map((rol) => rol.toString()) || []; + + const userData = user.toPersistenceData(); + + return { + user_id: userData.id, + email: userData.email, + username: userData.username, + }; + }, +}; diff --git a/packages/rdx-auth/src/presentation/dto/auth.request.dto.ts b/packages/rdx-auth/src/presentation/dto/auth.request.dto.ts new file mode 100644 index 0000000..c41713f --- /dev/null +++ b/packages/rdx-auth/src/presentation/dto/auth.request.dto.ts @@ -0,0 +1,10 @@ +export interface IRegisterUserRequestDTO { + username: string; + email: string; + password: string; +} + +export interface ILoginUserRequestDTO { + email: string; + password: string; +} diff --git a/packages/rdx-auth/src/presentation/dto/auth.response.dto.ts b/packages/rdx-auth/src/presentation/dto/auth.response.dto.ts new file mode 100644 index 0000000..6b4aeee --- /dev/null +++ b/packages/rdx-auth/src/presentation/dto/auth.response.dto.ts @@ -0,0 +1,24 @@ +export interface IRegisterUserResponseDTO { + user_id: string; + username: string; + email: string; +} + +export interface ILoginUserResponseDTO { + user: { + id: string; + username: string; + email: string; + tab_id: string; + }; + tokens: { + access_token: string; + refresh_token: string; + }; +} + +export interface ILogoutResponseDTO {} + +export interface IRefreshTokenResponseDTO { + refresh_token: string; +} diff --git a/packages/rdx-auth/src/presentation/dto/auth.validation.dto.ts b/packages/rdx-auth/src/presentation/dto/auth.validation.dto.ts new file mode 100644 index 0000000..96b1a3a --- /dev/null +++ b/packages/rdx-auth/src/presentation/dto/auth.validation.dto.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +export const RegisterUserSchema = z.object({ + username: z.string().min(3, "Username must be at least 3 characters long"), + email: z.string().email("Invalid email format"), + password: z.string().min(6, "Password must be at least 6 characters long"), +}); + +export const LoginUserSchema = z.object({ + email: z.string().email("Invalid email format"), + password: z.string().min(6, "Password must be at least 6 characters long"), +}); + +export const RefreshTokenSchema = z.object({ + refresh_token: z.string().min(1, "Refresh token is required"), +}); diff --git a/packages/rdx-auth/src/presentation/dto/index.ts b/packages/rdx-auth/src/presentation/dto/index.ts new file mode 100644 index 0000000..9e7426a --- /dev/null +++ b/packages/rdx-auth/src/presentation/dto/index.ts @@ -0,0 +1,7 @@ +export * from "./auth.request.dto"; +export * from "./auth.response.dto"; +export * from "./auth.validation.dto"; + +export * from "./user.request.dto"; +export * from "./user.response.dto"; +export * from "./user.validation.dto"; diff --git a/packages/rdx-auth/src/presentation/dto/user.request.dto.ts b/packages/rdx-auth/src/presentation/dto/user.request.dto.ts new file mode 100644 index 0000000..bcb6ea3 --- /dev/null +++ b/packages/rdx-auth/src/presentation/dto/user.request.dto.ts @@ -0,0 +1 @@ +export interface IListUsersRequestDTO {} diff --git a/packages/rdx-auth/src/presentation/dto/user.response.dto.ts b/packages/rdx-auth/src/presentation/dto/user.response.dto.ts new file mode 100644 index 0000000..5e68f30 --- /dev/null +++ b/packages/rdx-auth/src/presentation/dto/user.response.dto.ts @@ -0,0 +1,5 @@ +export interface IListUsersResponseDTO { + id: string; + username: string; + email: string; +} diff --git a/packages/rdx-auth/src/presentation/dto/user.validation.dto.ts b/packages/rdx-auth/src/presentation/dto/user.validation.dto.ts new file mode 100644 index 0000000..2ddd286 --- /dev/null +++ b/packages/rdx-auth/src/presentation/dto/user.validation.dto.ts @@ -0,0 +1,3 @@ +import { z } from "zod"; + +export const ListUsersSchema = z.object({}); diff --git a/packages/rdx-auth/src/presentation/index.ts b/packages/rdx-auth/src/presentation/index.ts new file mode 100644 index 0000000..a123289 --- /dev/null +++ b/packages/rdx-auth/src/presentation/index.ts @@ -0,0 +1,2 @@ +export * from "./controllers"; +export * from "./dto"; diff --git a/packages/rdx-auth/tsconfig.json b/packages/rdx-auth/tsconfig.json new file mode 100644 index 0000000..ae936c8 --- /dev/null +++ b/packages/rdx-auth/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["node"], + + "paths": { + "@/*": ["./src/*"] + } + }, + "files": ["src/index.ts"], + "include": ["src/index.ts"], + "exclude": ["node_modules", "dist", "**/*/__tests__"] +} diff --git a/packages/rdx-auth/turbo.json b/packages/rdx-auth/turbo.json new file mode 100644 index 0000000..35be9c6 --- /dev/null +++ b/packages/rdx-auth/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/packages/rdx-core/package.json b/packages/rdx-core/package.json new file mode 100644 index 0000000..663b9dd --- /dev/null +++ b/packages/rdx-core/package.json @@ -0,0 +1,80 @@ +{ + "name": "@rdx/core", + "version": "0.0.0", + "private": true, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist && rm -rf node_modules", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "jest": { + "preset": "@repo/jest-presets/node" + }, + "devDependencies": { + "@jest/globals": "^29.7.0", + "@repo/eslint-config": "workspace:*", + "@repo/jest-presets": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/bcrypt": "^5.0.2", + "@types/body-parser": "^1.19.5", + "@types/cors": "^2.8.17", + "@types/dinero.js": "^1.9.4", + "@types/express": "^4.17.21", + "@types/glob": "^8.1.0", + "@types/jest": "^29.5.14", + "@types/jsonwebtoken": "^9.0.9", + "@types/luxon": "^3.6.2", + "@types/morgan": "^1.9.9", + "@types/node": "^22.15.2", + "@types/passport": "^1.0.17", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", + "@types/response-time": "^2.3.8", + "@types/supertest": "^6.0.3", + "@typescript-eslint/eslint-plugin": "^8.31.0", + "@typescript-eslint/parser": "^8.31.0", + "esbuild": "^0.25.3", + "esbuild-register": "^3.6.0", + "eslint": "^9.25.1", + "jest": "^29.7.0", + "nodemon": "^3.1.10", + "supertest": "^7.1.0", + "typescript": "5.8.3" + }, + "dependencies": { + "@rdx/ddd-domain": "workspace:*", + "@rdx/logger": "workspace:*", + "@rdx/modules": "workspace:*", + "@rdx/utils": "workspace:*", + "bcrypt": "^5.1.1", + "body-parser": "^2.2.0", + "cors": "^2.8.5", + "dinero.js": "^1.9.1", + "dotenv": "^16.5.0", + "express": "^4.21.2", + "helmet": "^8.1.0", + "http-status": "^2.1.0", + "jsonwebtoken": "^9.0.2", + "libphonenumber-js": "^1.11.20", + "luxon": "^3.5.0", + "module-alias": "^2.2.3", + "mysql2": "^3.12.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", + "path": "^0.12.7", + "reflect-metadata": "^0.2.2", + "response-time": "^2.3.3", + "sequelize": "^6.37.7", + "zod": "^3.24.3" + } +} diff --git a/packages/rdx-core/src/index.ts b/packages/rdx-core/src/index.ts new file mode 100644 index 0000000..818ca8c --- /dev/null +++ b/packages/rdx-core/src/index.ts @@ -0,0 +1,2 @@ +export * from "./infrastructure"; +export * from "./presentation"; diff --git a/packages/rdx-core/src/infrastructure/database/index.ts b/packages/rdx-core/src/infrastructure/database/index.ts new file mode 100644 index 0000000..4648907 --- /dev/null +++ b/packages/rdx-core/src/infrastructure/database/index.ts @@ -0,0 +1,2 @@ +export * from "./transaction-manager"; +export * from "./transaction-manager.interface"; diff --git a/packages/rdx-core/src/infrastructure/database/transaction-manager.interface.ts b/packages/rdx-core/src/infrastructure/database/transaction-manager.interface.ts new file mode 100644 index 0000000..12c3970 --- /dev/null +++ b/packages/rdx-core/src/infrastructure/database/transaction-manager.interface.ts @@ -0,0 +1,27 @@ +export interface ITransactionManager { + /** + * 🔹 Inicia una transacción + */ + start(): Promise; + + /** + * 🔹 Obtiene la transacción activa + */ + getTransaction(): any; + + /** + * 🔹 Ejecuta un bloque de código dentro de una transacción + * Si algo falla, se hace rollback automáticamente. + */ + complete(work: (transaction: any) => Promise): Promise; + + /** + * 🔹 Confirma la transacción + */ + commit(): Promise; + + /** + * 🔹 Revierte la transacción + */ + rollback(): Promise; +} diff --git a/packages/rdx-core/src/infrastructure/database/transaction-manager.ts b/packages/rdx-core/src/infrastructure/database/transaction-manager.ts new file mode 100644 index 0000000..c22cd84 --- /dev/null +++ b/packages/rdx-core/src/infrastructure/database/transaction-manager.ts @@ -0,0 +1,60 @@ +import { ITransactionManager } from "./transaction-manager.interface"; + +export abstract class TransactionManager implements ITransactionManager { + protected _transaction: any | null = null; + + /** + * 🔹 Inicia una transacción si no hay una activa + */ + async start(): Promise { + if (!this._transaction) { + this._transaction = await this._startTransaction(); + } + } + + /** + * 🔹 Devuelve la transacción activa + */ + getTransaction(): any { + if (!this._transaction) { + throw new Error("No active transaction. Call start() first."); + } + return this._transaction; + } + + /** + * 🔹 Ejecuta una función dentro de una transacción + */ + async complete(work: (transaction: any) => Promise): Promise { + await this.start(); + try { + const result = await work(this.getTransaction()); + await this.commit(); + return result; + } catch (error) { + await this.rollback(); + throw error; + } + } + + /** + * 🔹 Métodos abstractos para manejar transacciones + */ + protected abstract _startTransaction(): Promise; + protected abstract _commitTransaction(): Promise; + protected abstract _rollbackTransaction(): Promise; + + async commit(): Promise { + if (this._transaction) { + await this._commitTransaction(); + this._transaction = null; + } + } + + async rollback(): Promise { + if (this._transaction) { + await this._rollbackTransaction(); + this._transaction = null; + } + } +} diff --git a/packages/rdx-core/src/infrastructure/index.ts b/packages/rdx-core/src/infrastructure/index.ts new file mode 100644 index 0000000..7b38a8f --- /dev/null +++ b/packages/rdx-core/src/infrastructure/index.ts @@ -0,0 +1,2 @@ +export * from "./database"; +export * from "./sequelize"; diff --git a/packages/rdx-core/src/infrastructure/sequelize/index.ts b/packages/rdx-core/src/infrastructure/sequelize/index.ts new file mode 100644 index 0000000..df6c2cb --- /dev/null +++ b/packages/rdx-core/src/infrastructure/sequelize/index.ts @@ -0,0 +1,3 @@ +export * from "./sequelize-mapper"; +export * from "./sequelize-repository"; +export * from "./sequelize-transaction-manager"; diff --git a/packages/rdx-core/src/infrastructure/sequelize/sequelize-mapper.ts b/packages/rdx-core/src/infrastructure/sequelize/sequelize-mapper.ts new file mode 100644 index 0000000..f120193 --- /dev/null +++ b/packages/rdx-core/src/infrastructure/sequelize/sequelize-mapper.ts @@ -0,0 +1,111 @@ +import { DomainEntity } from "@rdx/ddd-domain"; +import { Collection, Result } from "@rdx/utils"; +import { Model } from "sequelize"; + +export type MapperParamsType = Record; + +interface IDomainMapper> { + mapToDomain(source: TModel, params?: MapperParamsType): Result; + mapArrayToDomain(source: TModel[], params?: MapperParamsType): Result, Error>; + mapArrayAndCountToDomain( + source: TModel[], + totalCount: number, + params?: MapperParamsType + ): Result, Error>; +} + +interface IPersistenceMapper> { + mapToPersistence(source: TEntity, params?: MapperParamsType): TModelAttributes; + mapCollectionToPersistence( + source: Collection, + params?: MapperParamsType + ): TModelAttributes[]; +} + +export interface ISequelizeMapper< + TModel extends Model, + TModelAttributes, + TEntity extends DomainEntity, +> extends IDomainMapper, + IPersistenceMapper {} + +export abstract class SequelizeMapper< + TModel extends Model, + TModelAttributes, + TEntity extends DomainEntity, +> implements ISequelizeMapper +{ + public abstract mapToDomain(source: TModel, params?: MapperParamsType): Result; + + public mapArrayToDomain( + source: TModel[], + params?: MapperParamsType + ): Result, Error> { + return this.mapArrayAndCountToDomain(source, source.length, params); + } + + public mapArrayAndCountToDomain( + source: TModel[], + totalCount: number, + params?: MapperParamsType + ): Result, Error> { + try { + const items = source.map( + (value, index) => this.mapToDomain(value, { index, ...params }).data + ); + return Result.ok(new Collection(items, totalCount)); + } catch (error) { + return Result.fail(error as Error); + } + } + + public abstract mapToPersistence(source: TEntity, params?: MapperParamsType): TModelAttributes; + + public mapCollectionToPersistence( + source: Collection, + params?: MapperParamsType + ): TModelAttributes[] { + return source.map((value, index) => this.mapToPersistence(value, { index, ...params })); + } + + protected safeMap(operation: () => T, key: string): Result { + try { + return Result.ok(operation()); + } catch (error: unknown) { + return Result.fail(error as Error); + } + } + + protected mapsValue( + row: TModel, + key: string, + customMapFn: (value: any, params: MapperParamsType) => Result, + params: MapperParamsType = { defaultValue: null } + ): Result { + return customMapFn(row?.dataValues[key] ?? params.defaultValue, params); + } + + protected mapsAssociation( + row: TModel, + associationName: string, + customMapper: IDomainMapper, + params: MapperParamsType = {} + ): Result { + if (!customMapper) { + Result.fail(Error(`Custom mapper undefined for ${associationName}`)); + } + + const { filter, ...otherParams } = params; + let associationRows = row?.dataValues[associationName] ?? []; + + if (filter) { + associationRows = Array.isArray(associationRows) + ? associationRows.filter(filter) + : filter(associationRows); + } + + return Array.isArray(associationRows) + ? customMapper.mapArrayToDomain(associationRows, otherParams) + : customMapper.mapToDomain(associationRows, otherParams); + } +} diff --git a/packages/rdx-core/src/infrastructure/sequelize/sequelize-repository.ts b/packages/rdx-core/src/infrastructure/sequelize/sequelize-repository.ts new file mode 100644 index 0000000..57337df --- /dev/null +++ b/packages/rdx-core/src/infrastructure/sequelize/sequelize-repository.ts @@ -0,0 +1,156 @@ +import { IAggregateRootRepository, UniqueID } from "@rdx/ddd-domain"; +import { logger } from "@rdx/logger"; +import { Result } from "@rdx/utils"; +import { ModelDefined, Transaction } from "sequelize"; + +export abstract class SequelizeRepository implements IAggregateRootRepository { + protected async _findAll( + model: ModelDefined, + //queryCriteria?: IQueryCriteria, + params: any = {}, + transaction?: Transaction + ): Promise { + return model.findAll({ + transaction, + ...params, + }); + } + + protected _findById( + model: ModelDefined, + id: string, + params: any = {}, + transaction?: Transaction + ): Promise { + return model.findByPk(id.toString(), { + transaction, + ...params, + }); + } + + protected async _getBy( + model: ModelDefined, + field: string, + value: any, + params: any = {}, + transaction?: Transaction + ): Promise { + const where: { [key: string]: any } = {}; + + where[field] = value; + + return model.findOne({ + where, + transaction, + ...params, + }); + } + + protected async _getById( + model: ModelDefined, + id: UniqueID | string, + params: any = {}, + transaction?: Transaction + ): Promise { + return model.findByPk(id.toString(), { + transaction, + ...params, + }); + } + + protected async _exists( + model: ModelDefined, + field: string, + value: any, + transaction?: Transaction + ): Promise { + const where: { [key: string]: any } = {}; + where[field] = value; + + const count: number = await model.count({ + where, + transaction, + }); + + return Promise.resolve(Boolean(count !== 0)); + } + + protected async _save( + model: ModelDefined, + id: UniqueID, + data: any, + params: any = {}, + transaction?: Transaction + ): Promise { + if (await this._exists(model, "id", id.toString())) { + await model.update( + { + ...data, + id: undefined, + }, + { + where: { id: id.toString() }, + transaction: transaction, + ...params, + } + ); + } else { + await model.create( + { + ...data, + id: id.toString(), + }, + { + include: [{ all: true }], + transaction: transaction, + ...params, + } + ); + } + } + + protected async _deleteById( + model: ModelDefined, + id: UniqueID, + force: boolean = false, + transaction?: Transaction + ) { + await model.destroy({ + where: { + id: id.toString(), + }, + transaction, + force, + }); + } + + /** + * 🔹 Manejo de errores de la base de datos con personalización + * @param error Error lanzado por Sequelize + * @param errorMapper Función opcional para personalizar errores específicos + */ + protected _handleDatabaseError( + error: unknown, + errorMapper?: (error: Error) => string | null + ): Result { + const _error = error as Error; + + logger.error(`Database error: ${_error.message}`); + + // Si la clase hija proporciona un mapeo personalizado, lo usa + if (errorMapper) { + const mappedMessage = errorMapper(_error); + if (mappedMessage) { + return Result.fail(new Error(mappedMessage)); + } + } + + // Mapa de errores genéricos para la base de datos + const ERROR_MAP = new Map([ + ["SequelizeConnectionError", "Database connection lost"], + ["SequelizeTimeoutError", "Database request timed out"], + ]); + + return Result.fail(new Error(ERROR_MAP.get(_error.name) || "Unexpected database error")); + } +} diff --git a/packages/rdx-core/src/infrastructure/sequelize/sequelize-transaction-manager.ts b/packages/rdx-core/src/infrastructure/sequelize/sequelize-transaction-manager.ts new file mode 100644 index 0000000..a081054 --- /dev/null +++ b/packages/rdx-core/src/infrastructure/sequelize/sequelize-transaction-manager.ts @@ -0,0 +1,27 @@ +import { Sequelize, Transaction } from "sequelize"; +import { TransactionManager } from "../database"; + +export class SequelizeTransactionManager extends TransactionManager { + protected _database: any | null = null; + + protected async _startTransaction(): Promise { + return await this._database.transaction(); + } + + protected async _commitTransaction(): Promise { + if (this._transaction) { + await this._transaction.commit(); + } + } + + protected async _rollbackTransaction(): Promise { + if (this._transaction) { + await this._transaction.rollback(); + } + } + + constructor(database: Sequelize) { + super(); + this._database = database; + } +} diff --git a/packages/rdx-core/src/presentation/dto/error.dto.ts b/packages/rdx-core/src/presentation/dto/error.dto.ts new file mode 100644 index 0000000..effa892 --- /dev/null +++ b/packages/rdx-core/src/presentation/dto/error.dto.ts @@ -0,0 +1,20 @@ +export interface IErrorDTO { + detail?: string; + instance?: string; + status: number; + title: string; + type?: string; + context: IErrorContextDTO; + extra: IErrorExtraDTO; +} + +export interface IErrorContextDTO { + user?: unknown; + params?: Record; + query?: Record; + body?: Record; +} + +export interface IErrorExtraDTO { + errors: Record[]; +} diff --git a/packages/rdx-core/src/presentation/dto/index.ts b/packages/rdx-core/src/presentation/dto/index.ts new file mode 100644 index 0000000..6961457 --- /dev/null +++ b/packages/rdx-core/src/presentation/dto/index.ts @@ -0,0 +1,2 @@ +export * from "./error.dto"; +export * from "./types.dto"; diff --git a/packages/rdx-core/src/presentation/dto/types.dto.ts b/packages/rdx-core/src/presentation/dto/types.dto.ts new file mode 100644 index 0000000..e3c67db --- /dev/null +++ b/packages/rdx-core/src/presentation/dto/types.dto.ts @@ -0,0 +1,15 @@ +export interface IMoneyDTO { + amount: number | null; + scale: number; + currency_code: string; +} + +export interface IPercentageDTO { + amount: number | null; + scale: number; +} + +export interface IQuantityDTO { + amount: number | null; + scale: number; +} diff --git a/packages/rdx-core/src/presentation/express/api-error.ts b/packages/rdx-core/src/presentation/express/api-error.ts new file mode 100644 index 0000000..6cac5a8 --- /dev/null +++ b/packages/rdx-core/src/presentation/express/api-error.ts @@ -0,0 +1,37 @@ +interface IApiErrorOptions { + status: number; + title: string; + detail: string; + type?: string; + instance?: string; + errors?: any[]; + [key: string]: any; // Para permitir añadir campos extra +} + +export class ApiError extends Error { + public status: number; + public title: string; + public detail: string; + public type: string; + public instance?: string; + public errors?: any[]; + public timestamp: string; + + constructor(options: IApiErrorOptions) { + super(options.title); + + // Asegura que la instancia sea reconocida correctamente como ApiError + Object.setPrototypeOf(this, ApiError.prototype); + + // Campos obligatorios + this.status = options.status; + this.title = options.title; + this.detail = options.detail; + this.timestamp = new Date().toISOString(); + + // Campos opcionales con valores por defecto + this.type = options.type ?? "about:blank"; + this.instance = options.instance; + this.errors = options.errors; + } +} diff --git a/packages/rdx-core/src/presentation/express/express-controller.ts b/packages/rdx-core/src/presentation/express/express-controller.ts new file mode 100644 index 0000000..fea02f3 --- /dev/null +++ b/packages/rdx-core/src/presentation/express/express-controller.ts @@ -0,0 +1,166 @@ +import { logger } from "@rdx/logger"; +import { NextFunction, Request, Response } from "express"; +import httpStatus from "http-status"; +import { ApiError } from "./api-error"; + +export abstract class ExpressController { + protected req!: Request; // | AuthenticatedRequest | TabContextRequest; + protected res!: Response; + protected next!: NextFunction; + + static errorResponse(apiError: ApiError, res: Response) { + logger.error(`[${apiError.status}] ${apiError.title}: ${apiError.detail}`); + return res.status(apiError.status).json(apiError); + } + + protected abstract executeImpl(): Promise; + + protected ok(dto?: T) { + return dto ? this.res.status(httpStatus.OK).json(dto) : this.res.status(httpStatus.OK).send(); + } + + protected created(dto?: T) { + return dto + ? this.res.status(httpStatus.CREATED).json(dto) + : this.res.status(httpStatus.CREATED).send(); + } + + protected noContent() { + return this.res.status(httpStatus.NO_CONTENT).send(); + } + + /** + * 🔹 Respuesta para errores de cliente (400 Bad Request) + */ + public clientError(message: string, errors?: any[] | any) { + return ExpressController.errorResponse( + new ApiError({ + status: 400, + title: "Bad Request", + detail: message, + errors: Array.isArray(errors) ? errors : [errors], + }), + this.res + ); + } + + /** + * 🔹 Respuesta para errores de autenticación (401 Unauthorized) + */ + protected unauthorizedError(message?: string) { + return ExpressController.errorResponse( + new ApiError({ + status: 401, + title: httpStatus["401"], + name: httpStatus["401_NAME"], + detail: message ?? httpStatus["401_MESSAGE"], + }), + this.res + ); + } + + /** + * 🔹 Respuesta para errores de autorización (403 Forbidden) + */ + protected forbiddenError(message?: string) { + return ExpressController.errorResponse( + new ApiError({ + status: 403, + title: "Forbidden", + detail: message ?? "You do not have permission to perform this action.", + }), + this.res + ); + } + + /** + * 🔹 Respuesta para recursos no encontrados (404 Not Found) + */ + protected notFoundError(message: string) { + return ExpressController.errorResponse( + new ApiError({ + status: 404, + title: "Not Found", + detail: message, + }), + this.res + ); + } + + /** + * 🔹 Respuesta para conflictos (409 Conflict) + */ + protected conflictError(message: string, errors?: any[]) { + return ExpressController.errorResponse( + new ApiError({ + status: 409, + title: "Conflict", + detail: message, + errors, + }), + this.res + ); + } + + /** + * 🔹 Respuesta para errores de validación de entrada (422 Unprocessable Entity) + */ + protected invalidInputError(message: string, errors?: any[]) { + return ExpressController.errorResponse( + new ApiError({ + status: 422, + title: httpStatus["422"], + name: httpStatus["422_NAME"], + detail: message ?? httpStatus["422_MESSAGE"], + errors, + }), + this.res + ); + } + + /** + * Respuesta para errores de servidor no disponible (503 Service Unavailable) + * @param message + * @returns + */ + protected unavailableError(message?: string) { + return ExpressController.errorResponse( + new ApiError({ + status: 503, + title: httpStatus["503"], + name: httpStatus["503_NAME"], + detail: message ?? httpStatus["503_MESSAGE"], + }), + this.res + ); + } + + /** + * 🔹 Respuesta para errores internos del servidor (500 Internal Server Error) + */ + protected internalServerError(message?: string) { + return ExpressController.errorResponse( + new ApiError({ + status: 500, + title: httpStatus["500"], + name: httpStatus["500_NAME"], + detail: message ?? httpStatus["500_MESSAGE"], + }), + this.res + ); + } + + public execute(req: Request, res: Response, next: NextFunction): void { + this.req = req; + this.res = res; + this.next = next; + + try { + this.executeImpl(); + } catch (error: unknown) { + const _error = error as Error; + logger.error(`Unhandled error in controller: ${_error.message}`); + this.internalServerError(_error.message); + } + } +} diff --git a/packages/rdx-core/src/presentation/express/index.ts b/packages/rdx-core/src/presentation/express/index.ts new file mode 100644 index 0000000..3331f2c --- /dev/null +++ b/packages/rdx-core/src/presentation/express/index.ts @@ -0,0 +1,4 @@ +export * from "./api-error"; +export * from "./express-controller"; +export * from "./middlewares"; +export * from "./validate-request-dto"; diff --git a/packages/rdx-core/src/presentation/express/middlewares/global-error-handler.ts b/packages/rdx-core/src/presentation/express/middlewares/global-error-handler.ts new file mode 100644 index 0000000..f6717a5 --- /dev/null +++ b/packages/rdx-core/src/presentation/express/middlewares/global-error-handler.ts @@ -0,0 +1,39 @@ +import { NextFunction, Request, Response } from "express"; +import { ApiError } from "../api-error"; +import { logger } from "@rdx/logger"; + +export const globalErrorHandler = async ( + error: Error, + req: Request, + res: Response, + next: NextFunction +) => { + // Si ya se envió una respuesta, delegamos al siguiente error handler + if (res.headersSent) { + return next(error); + } + + logger.error(`❌ Unhandled API error: ${error.message}`); + + // Verifica si el error es una instancia de ApiError + if (error instanceof ApiError) { + // Respuesta con formato RFC 7807 + return res.status(error.status).json({ + type: error.type, + title: error.title, + status: error.status, + detail: error.detail, + instance: error.instance ?? req.originalUrl, + errors: error.errors ?? [], // Aquí puedes almacenar validaciones, etc. + }); + } + + // Si no es un ApiError, lo tratamos como un error interno (500) + return res.status(500).json({ + type: "https://example.com/probs/internal-server-error", + title: "Internal Server Error", + status: 500, + detail: error.message || "Ha ocurrido un error inesperado.", + instance: req.originalUrl, + }); +}; diff --git a/packages/rdx-core/src/presentation/express/middlewares/index.ts b/packages/rdx-core/src/presentation/express/middlewares/index.ts new file mode 100644 index 0000000..de65552 --- /dev/null +++ b/packages/rdx-core/src/presentation/express/middlewares/index.ts @@ -0,0 +1 @@ +export * from "./global-error-handler"; diff --git a/packages/rdx-core/src/presentation/express/validate-request-dto.ts b/packages/rdx-core/src/presentation/express/validate-request-dto.ts new file mode 100644 index 0000000..7ff5f04 --- /dev/null +++ b/packages/rdx-core/src/presentation/express/validate-request-dto.ts @@ -0,0 +1,38 @@ +import { NextFunction, Request, Response } from "express"; +import httpStatus from "http-status"; +import { ZodSchema } from "zod"; +import { ApiError } from "./api-error"; + +export const validateAndParseBody = + (schema: ZodSchema, options?: { sanitize?: boolean }) => + (req: Request, res: Response, next: NextFunction) => { + const result = schema.safeParse(req.body); + try { + if (!result.success) { + // Construye errores detallados + const validationErrors = result.error.errors.map((err) => ({ + field: err.path.join("."), + message: err.message, + })); + + throw new ApiError({ + status: httpStatus.BAD_REQUEST, //400 + title: "Validation Error", + detail: "Algunos campos no cumplen con los criterios de validación.", + type: "https://example.com/probs/validation-error", + instance: req.originalUrl, + errors: validationErrors, + }); + } + + // Si pasa la validación, opcionalmente reescribe req.body + if (options?.sanitize ?? true) { + req.body = result.data; + } + + next(); + } catch (error: unknown) { + // Si ocurre un error, delega al manejador de errores global + next(error as ApiError); + } + }; diff --git a/packages/rdx-core/src/presentation/index.ts b/packages/rdx-core/src/presentation/index.ts new file mode 100644 index 0000000..cc1f9dc --- /dev/null +++ b/packages/rdx-core/src/presentation/index.ts @@ -0,0 +1,2 @@ +export * from "./dto"; +export * from "./express"; diff --git a/packages/rdx-core/tsconfig.json b/packages/rdx-core/tsconfig.json new file mode 100644 index 0000000..ae936c8 --- /dev/null +++ b/packages/rdx-core/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["node"], + + "paths": { + "@/*": ["./src/*"] + } + }, + "files": ["src/index.ts"], + "include": ["src/index.ts"], + "exclude": ["node_modules", "dist", "**/*/__tests__"] +} diff --git a/packages/rdx-core/turbo.json b/packages/rdx-core/turbo.json new file mode 100644 index 0000000..35be9c6 --- /dev/null +++ b/packages/rdx-core/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/packages/rdx-criteria/package.json b/packages/rdx-criteria/package.json new file mode 100644 index 0000000..e2e7167 --- /dev/null +++ b/packages/rdx-criteria/package.json @@ -0,0 +1,33 @@ +{ + "name": "@rdx/criteria", + "version": "0.0.0", + "private": true, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist && rm -rf node_modules", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "jest": { + "preset": "@repo/jest-presets/node" + }, + "devDependencies": { + "@changesets/cli": "^2.29.2", + "@faker-js/faker": "^9.7.0", + "@repo/eslint-config": "workspace:*", + "@repo/jest-presets": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/jest": "^29.5.14", + "@types/node": "^22.15.2", + "jest": "^29.7.0", + "typescript": "^5.8.3" + } +} diff --git a/packages/rdx-criteria/src/criteria.ts b/packages/rdx-criteria/src/criteria.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/rdx-criteria/src/index.ts b/packages/rdx-criteria/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/rdx-criteria/src/pagination.ts/index.ts b/packages/rdx-criteria/src/pagination.ts/index.ts new file mode 100644 index 0000000..85731be --- /dev/null +++ b/packages/rdx-criteria/src/pagination.ts/index.ts @@ -0,0 +1,5 @@ +export interface ICriteriaProps {} + +export interface ICriteria {} + +class Criteria implements ICriteria {} diff --git a/packages/rdx-criteria/src/pagination.ts/pagination-defaults.ts b/packages/rdx-criteria/src/pagination.ts/pagination-defaults.ts new file mode 100644 index 0000000..b632747 --- /dev/null +++ b/packages/rdx-criteria/src/pagination.ts/pagination-defaults.ts @@ -0,0 +1,7 @@ +export const INITIAL_PAGE_INDEX = 0; +export const INITIAL_PAGE_SIZE = 10; + +export const MIN_PAGE_INDEX = 0; +export const MIN_PAGE_SIZE = 1; + +export const MAX_PAGE_SIZE = 9999; //Number.MAX_SAFE_INTEGER; diff --git a/packages/rdx-criteria/tsconfig.json b/packages/rdx-criteria/tsconfig.json new file mode 100644 index 0000000..ae936c8 --- /dev/null +++ b/packages/rdx-criteria/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["node"], + + "paths": { + "@/*": ["./src/*"] + } + }, + "files": ["src/index.ts"], + "include": ["src/index.ts"], + "exclude": ["node_modules", "dist", "**/*/__tests__"] +} diff --git a/packages/rdx-ddd-domain/package.json b/packages/rdx-ddd-domain/package.json new file mode 100644 index 0000000..2f1691a --- /dev/null +++ b/packages/rdx-ddd-domain/package.json @@ -0,0 +1,40 @@ +{ + "name": "@rdx/ddd-domain", + "version": "0.0.0", + "private": true, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist && rm -rf node_modules", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "jest": { + "preset": "@repo/jest-presets/node" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/jest-presets": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/dinero.js": "^1.9.4", + "@types/node": "^22.15.2", + "jest": "^29.7.0", + "typescript": "^5.8.3" + }, + "dependencies": { + "@rdx/logger": "workspace:*", + "@rdx/utils": "workspace:*", + "dinero.js": "^1.9.1", + "libphonenumber-js": "^1.12.7", + "shallow-equal-object": "^1.1.1", + "uuid": "^11.1.0", + "zod": "^3.24.3" + } +} diff --git a/packages/rdx-ddd-domain/src/aggregate-root-repository.interface.ts b/packages/rdx-ddd-domain/src/aggregate-root-repository.interface.ts new file mode 100644 index 0000000..845c7ae --- /dev/null +++ b/packages/rdx-ddd-domain/src/aggregate-root-repository.interface.ts @@ -0,0 +1 @@ +export interface IAggregateRootRepository {} diff --git a/packages/rdx-ddd-domain/src/aggregate-root.ts b/packages/rdx-ddd-domain/src/aggregate-root.ts new file mode 100644 index 0000000..f27a50b --- /dev/null +++ b/packages/rdx-ddd-domain/src/aggregate-root.ts @@ -0,0 +1,39 @@ +import { logger } from "@rdx/logger"; +import { DomainEntity } from "./domain-entity"; +import { IDomainEvent } from "./events"; + +export abstract class AggregateRoot extends DomainEntity { + private _domainEvents: IDomainEvent[] = []; + + private logDomainEventAdded(event: IDomainEvent): void { + const thisClass = Reflect.getPrototypeOf(this); + const domainEventClass = Reflect.getPrototypeOf(event); + logger.info( + `[Domain Event Created]: ${thisClass?.constructor.name} ==> ${domainEventClass?.constructor.name}` + ); + } + + /** + * 🔹 Agregar un evento de dominio al agregado + */ + protected addDomainEvent(event: IDomainEvent): void { + this._domainEvents.push(event); + + // Log the domain event + this.logDomainEventAdded(event); + } + + /** + * 🔹 Obtener los eventos de dominio pendientes + */ + get domainEvents(): IDomainEvent[] { + return this._domainEvents; + } + + /** + * 🔹 Limpiar la lista de eventos después de procesarlos + */ + public clearDomainEvents(): void { + this._domainEvents.splice(0, this._domainEvents.length); + } +} diff --git a/packages/rdx-ddd-domain/src/domain-entity.ts b/packages/rdx-ddd-domain/src/domain-entity.ts new file mode 100644 index 0000000..d7d7dcf --- /dev/null +++ b/packages/rdx-ddd-domain/src/domain-entity.ts @@ -0,0 +1,33 @@ +import { UniqueID } from "./value-objects/unique-id"; + +export abstract class DomainEntity { + protected readonly props: T; + public readonly id: UniqueID; + + protected constructor(props: T, id?: UniqueID) { + this.id = id ? id : UniqueID.generateNewID().data; + this.props = props; + } + + protected _flattenProps(props: T): { [s: string]: any } { + return Object.entries(props).reduce((result: any, [key, valueObject]) => { + console.log(key, valueObject.value); + result[key] = valueObject.value; + + return result; + }, {}); + } + + equals(other: DomainEntity): boolean { + return other instanceof DomainEntity && this.id.equals(other.id); + } + + toString(): { [s: string]: string } { + const flattenProps = this._flattenProps(this.props); + + return { + id: this.id.toString(), + ...flattenProps.map((prop: any) => String(prop)), + }; + } +} diff --git a/packages/rdx-ddd-domain/src/events/domain-event-handle.ts b/packages/rdx-ddd-domain/src/events/domain-event-handle.ts new file mode 100644 index 0000000..4301cdc --- /dev/null +++ b/packages/rdx-ddd-domain/src/events/domain-event-handle.ts @@ -0,0 +1,3 @@ +export interface IHandle { + setupSubscriptions(): void; +} diff --git a/packages/rdx-ddd-domain/src/events/domain-event.interface.ts b/packages/rdx-ddd-domain/src/events/domain-event.interface.ts new file mode 100644 index 0000000..8a2730d --- /dev/null +++ b/packages/rdx-ddd-domain/src/events/domain-event.interface.ts @@ -0,0 +1,7 @@ +import { UniqueID } from "../value-objects/unique-id"; + +export interface IDomainEvent { + eventName: string; // Nombre del evento + aggregateId: UniqueID; // ID del agregado que generó el evento + occurredAt: Date; // Fecha y hora del evento +} diff --git a/packages/rdx-ddd-domain/src/events/domain-event.ts b/packages/rdx-ddd-domain/src/events/domain-event.ts new file mode 100644 index 0000000..cb98929 --- /dev/null +++ b/packages/rdx-ddd-domain/src/events/domain-event.ts @@ -0,0 +1,135 @@ +// https://khalilstemmler.com/articles/typescript-domain-driven-design/chain-business-logic-domain-events/ + +import { AggregateRoot } from "../aggregate-root"; +import { UniqueID } from "../value-objects/unique-id"; +import { IDomainEvent } from "./domain-event.interface"; + +export class DomainEvents { + private static handlersMap: { [key: string]: Array<(event: IDomainEvent) => void> } = {}; + private static markedAggregates: AggregateRoot[] = []; + + /** + * @method markAggregateForDispatch + * @static + * @desc Called by aggregate root objects that have created domain + * events to eventually be dispatched when the infrastructure commits + * the unit of work. + */ + + public static markAggregateForDispatch(aggregate: AggregateRoot): void { + const aggregateFound = !!this.findMarkedAggregateByID(aggregate.id); + + if (!aggregateFound) { + this.markedAggregates.push(aggregate); + } + } + + /** + * @method dispatchAggregateEvents + * @static + * @private + * @desc Call all of the handlers for any domain events on this aggregate. + */ + + private static dispatchAggregateEvents(aggregate: AggregateRoot): void { + aggregate.domainEvents.forEach((event: IDomainEvent) => this.dispatch(event)); + } + + /** + * @method removeAggregateFromMarkedDispatchList + * @static + * @desc Removes an aggregate from the marked list. + */ + + private static removeAggregateFromMarkedDispatchList(aggregate: AggregateRoot): void { + const index = this.markedAggregates.findIndex((a) => a.equals(aggregate)); + + this.markedAggregates.splice(index, 1); + } + + /** + * @method findMarkedAggregateByID + * @static + * @desc Finds an aggregate within the list of marked aggregates. + */ + + private static findMarkedAggregateByID(id: UniqueID): AggregateRoot { + let found!: AggregateRoot; + + for (let aggregate of this.markedAggregates) { + if (aggregate.id.equals(id)) { + found = aggregate; + } + } + + return found; + } + + /** + * @method dispatchEventsForAggregate + * @static + * @desc When all we know is the ID of the aggregate, call this + * in order to dispatch any handlers subscribed to events on the + * aggregate. + */ + + public static dispatchEventsForAggregate(id: UniqueID): void { + const aggregate = this.findMarkedAggregateByID(id); + + if (aggregate) { + this.dispatchAggregateEvents(aggregate); + aggregate.clearDomainEvents(); + this.removeAggregateFromMarkedDispatchList(aggregate); + } + } + + /** + * @method register + * @static + * @desc Register a handler to a domain event. + */ + + public static register(callback: (event: IDomainEvent) => void, eventClassName: string): void { + if (!this.handlersMap.hasOwnProperty(eventClassName)) { + this.handlersMap[eventClassName] = []; + } + this.handlersMap[eventClassName].push(callback); + } + + /** + * @method clearHandlers + * @static + * @desc Useful for testing. + */ + + public static clearHandlers(): void { + this.handlersMap = {}; + } + + /** + * @method clearMarkedAggregates + * @static + * @desc Useful for testing. + */ + + public static clearMarkedAggregates(): void { + this.markedAggregates = []; + } + + /** + * @method dispatch + * @static + * @desc Invokes all of the subscribers to a particular domain event. + */ + + private static dispatch(event: IDomainEvent): void { + const eventClassName: string = event.constructor.name; + + if (this.handlersMap.hasOwnProperty(eventClassName)) { + const handlers: any[] = this.handlersMap[eventClassName]; + for (let handler of handlers) { + handler(event); + } + } + } +} diff --git a/packages/rdx-ddd-domain/src/events/index.ts b/packages/rdx-ddd-domain/src/events/index.ts new file mode 100644 index 0000000..1ba9786 --- /dev/null +++ b/packages/rdx-ddd-domain/src/events/index.ts @@ -0,0 +1,2 @@ +export * from "./domain-event"; +export * from "./domain-event.interface"; diff --git a/packages/rdx-ddd-domain/src/index.ts b/packages/rdx-ddd-domain/src/index.ts new file mode 100644 index 0000000..8f115f9 --- /dev/null +++ b/packages/rdx-ddd-domain/src/index.ts @@ -0,0 +1,5 @@ +export * from "./aggregate-root"; +export * from "./aggregate-root-repository.interface"; +export * from "./domain-entity"; +export * from "./events/domain-event.interface"; +export * from "./value-objects"; diff --git a/packages/rdx-ddd-domain/src/value-objects/__tests__/email-address.test.ts b/packages/rdx-ddd-domain/src/value-objects/__tests__/email-address.test.ts new file mode 100644 index 0000000..20b5957 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/__tests__/email-address.test.ts @@ -0,0 +1,61 @@ +import { EmailAddress } from "./email-address"; + +describe("EmailAddress Value Object", () => { + it("should create a valid email", () => { + const result = EmailAddress.create("user@example.com"); + + expect(result.isSuccess).toBe(true); + expect(result.data.getValue()).toBe("user@example.com"); + }); + + it("should return an error for invalid email format", () => { + const result = EmailAddress.create("invalid-email"); + + expect(result.isFailure).toBe(true); + expect(result.error.message).toBe("Invalid email format"); + }); + + it("should allow null email", () => { + const result = EmailAddress.createNullable(); + expect(result.isSuccess).toBe(true); + expect(result.data.getOrUndefined()).toBeUndefined(); + }); + + it("should return an error for empty string", () => { + const result = EmailAddress.create(""); + + expect(result.isSuccess).toBe(false); + }); + + it("should compare two equal email objects correctly", () => { + const email1 = EmailAddress.create("test@example.com"); + const email2 = EmailAddress.create("test@example.com"); + + expect(email1.isSuccess).toBe(true); + expect(email2.isSuccess).toBe(true); + expect(email1.data.equals(email2.data)).toBe(true); + }); + + it("should compare two different email objects as not equal", () => { + const email1 = EmailAddress.create("test@example.com"); + const email2 = EmailAddress.create("other@example.com"); + + expect(email1.isSuccess).toBe(true); + expect(email2.isSuccess).toBe(true); + expect(email1.data.equals(email2.data)).toBe(false); + }); + + it("should detect empty email correctly", () => { + const email = EmailAddress.createNullable(); + + expect(email.isSuccess).toBe(true); + expect(email.data.isSome()).toBe(false); + }); + + it("should detect non-empty email correctly", () => { + const email = EmailAddress.create("test@example.com"); + + expect(email.isSuccess).toBe(true); + expect(email.data.getValue()).toBe("test@example.com"); + }); +}); diff --git a/packages/rdx-ddd-domain/src/value-objects/__tests__/money-value.test.ts b/packages/rdx-ddd-domain/src/value-objects/__tests__/money-value.test.ts new file mode 100644 index 0000000..69ffda1 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/__tests__/money-value.test.ts @@ -0,0 +1,54 @@ +import { MoneyValue } from "../money-value"; + +describe("MoneyValue", () => { + test("should correctly instantiate with amount, scale, and currency", () => { + const money = new MoneyValue({ amount: 10000, scale: 2, currency_code: "EUR" }); + expect(money.amount).toBe(100); + expect(money.currency).toBe("EUR"); + expect(money.scale).toBe(2); + }); + + test("should add two MoneyValue instances with the same currency", () => { + const money1 = new MoneyValue({ amount: 10000, scale: 2, currency_code: "EUR" }); + const money2 = new MoneyValue({ amount: 5000, scale: 2, currency_code: "EUR" }); + const result = money1.add(money2); + expect(result.amount).toBe(150); + }); + + test("should subtract two MoneyValue instances with the same currency", () => { + const money1 = new MoneyValue({ amount: 20000, scale: 2, currency_code: "EUR" }); + const money2 = new MoneyValue({ amount: 5000, scale: 2, currency_code: "EUR" }); + const result = money1.subtract(money2); + expect(result.amount).toBe(150); + }); + + test("should throw an error when adding different currencies", () => { + const money1 = new MoneyValue({ amount: 10000, scale: 2, currency_code: "EUR" }); + const money2 = new MoneyValue({ amount: 5000, scale: 2, currency_code: "USD" }); + expect(() => money1.add(money2)).toThrow( + "You must provide a Dinero instance with the same currency" + ); + }); + + test("should correctly convert scale", () => { + const money = new MoneyValue({ amount: 10000, scale: 2, currency_code: "EUR" }); + const converted = money.convertScale(4); + expect(converted.amount).toBe(100); + expect(converted.scale).toBe(4); + }); + + test("should format correctly according to locale", () => { + const money = new MoneyValue({ amount: 123456, scale: 2, currency_code: "EUR" }); + expect(money.format("en-US")).toBe("€1,234.56"); + }); + + test("should compare MoneyValue instances correctly", () => { + const money1 = new MoneyValue({ amount: 10000, scale: 2, currency_code: "EUR" }); + const money2 = new MoneyValue({ amount: 10000, scale: 2, currency_code: "EUR" }); + const money3 = new MoneyValue({ amount: 5000, scale: 2, currency_code: "EUR" }); + + expect(money1.equalsTo(money2)).toBe(true); + expect(money1.greaterThan(money3)).toBe(true); + expect(money3.lessThan(money1)).toBe(true); + }); +}); diff --git a/packages/rdx-ddd-domain/src/value-objects/__tests__/name.spec.ts b/packages/rdx-ddd-domain/src/value-objects/__tests__/name.spec.ts new file mode 100644 index 0000000..5115510 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/__tests__/name.spec.ts @@ -0,0 +1,41 @@ +import { Name } from "../name"; + +describe("Name Value Object", () => { + test("Debe crear un Name válido", () => { + const nameResult = Name.create("John Doe"); + expect(nameResult.isSuccess).toBe(true); + expect(nameResult.data.getValue()).toBe("John Doe"); + }); + + test("Debe fallar si el nombre excede los 255 caracteres", () => { + const longName = "A".repeat(256); + const nameResult = Name.create(longName); + expect(nameResult.isSuccess).toBe(false); + expect(nameResult.error).toBeInstanceOf(Error); + }); + + test("Debe permitir un Name nullable vacío", () => { + const nullableNameResult = Name.createNullable(""); + expect(nullableNameResult.isSuccess).toBe(true); + expect(nullableNameResult.data.isSome()).toBe(false); + }); + + test("Debe permitir un Name nullable con un valor válido", () => { + const nullableNameResult = Name.createNullable("Alice"); + expect(nullableNameResult.isSuccess).toBe(true); + expect(nullableNameResult.data.isSome()).toBe(true); + expect(nullableNameResult.data.getOrUndefined()?.toString()).toBe("Alice"); + }); + + test("Debe generar acrónimos correctamente", () => { + expect(Name.generateAcronym("John Doe")).toBe("JDXX"); + expect(Name.generateAcronym("Alice Bob Charlie")).toBe("ABCX"); + expect(Name.generateAcronym("A B C D E")).toBe("ABCD"); + expect(Name.generateAcronym("SingleWord")).toBe("SXXX"); + }); + + test("Debe obtener el acrónimo de una instancia de Name", () => { + const name = Name.create("John Doe").data; + expect(name.getAcronym()).toBe("JDXX"); + }); +}); diff --git a/packages/rdx-ddd-domain/src/value-objects/__tests__/percentage.test.ts b/packages/rdx-ddd-domain/src/value-objects/__tests__/percentage.test.ts new file mode 100644 index 0000000..dcda91d --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/__tests__/percentage.test.ts @@ -0,0 +1,51 @@ +import { Percentage } from "../percentage"; // Ajusta la ruta según sea necesario + +describe("Percentage Value Object", () => { + test("Debe crear un porcentaje válido con escala por defecto", () => { + const result = Percentage.create({ amount: 200 }); // 2.00% + expect(result.isSuccess).toBe(true); + expect(result.data?.toString()).toBe("2.00%"); + }); + + test("Debe crear un porcentaje válido con escala definida", () => { + const result = Percentage.create({ amount: 2150, scale: 2 }); // 21.50% + expect(result.isSuccess).toBe(true); + expect(result.data?.toString()).toBe("21.50%"); + }); + + test("Debe devolver error si la cantidad supera el 100%", () => { + const result = Percentage.create({ amount: 48732000, scale: 4 }); + expect(result.isSuccess).toBe(false); + expect(result.error.message).toBe("La escala debe estar entre 0 y 2."); + }); + + test("Debe devolver error si la cantidad es negativa", () => { + const result = Percentage.create({ amount: -100, scale: 2 }); + expect(result.isSuccess).toBe(false); + expect(result.error.message).toContain("La cantidad no puede ser negativa."); + }); + + test("Debe devolver error si la escala es menor a 0", () => { + const result = Percentage.create({ amount: 100, scale: -1 }); + expect(result.isSuccess).toBe(false); + expect(result.error.message).toContain("Number must be greater than or equal to 0"); + }); + + test("Debe devolver error si la escala es mayor a 10", () => { + const result = Percentage.create({ amount: 100, scale: 11 }); + expect(result.isSuccess).toBe(false); + expect(result.error.message).toContain("La escala debe estar entre 0 y 2."); + }); + + test("Debe representar correctamente el valor como string", () => { + const result = Percentage.create({ amount: 750, scale: 2 }); // 7.50% + expect(result.isSuccess).toBe(true); + expect(result.data?.toString()).toBe("7.50%"); + }); + + test("Debe manejar correctamente el caso de 0%", () => { + const result = Percentage.create({ amount: 0, scale: 2 }); + expect(result.isSuccess).toBe(true); + expect(result.data?.toString()).toBe("0.00%"); + }); +}); diff --git a/packages/rdx-ddd-domain/src/value-objects/__tests__/phone-number.test.ts b/packages/rdx-ddd-domain/src/value-objects/__tests__/phone-number.test.ts new file mode 100644 index 0000000..0015b97 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/__tests__/phone-number.test.ts @@ -0,0 +1,70 @@ +import { parsePhoneNumberWithError } from "libphonenumber-js"; +import { Maybe } from "../../helpers/maybe"; +import { PhoneNumber } from "../phone-number"; + +describe("PhoneNumber", () => { + const validPhone = "+14155552671"; // Número válido en formato internacional + const invalidPhone = "12345"; // Número inválido + const nullablePhone = ""; + + test("debe crear un número de teléfono válido", () => { + const result = PhoneNumber.create(validPhone); + expect(result.isSuccess).toBe(true); + expect(result.data).toBeInstanceOf(PhoneNumber); + }); + + test("debe fallar al crear un número de teléfono inválido", () => { + const result = PhoneNumber.create(invalidPhone); + expect(result.isFailure).toBe(true); + expect(result.getError()?.message).toBe( + "Please specify a valid phone number (include the international prefix)." + ); + }); + + test("debe devolver None para valores nulos o vacíos", () => { + const result = PhoneNumber.createNullable(nullablePhone); + expect(result.isSuccess).toBe(true); + expect(result.data).toEqual(Maybe.none()); + }); + + test("debe devolver Some con un número de teléfono válido", () => { + const result = PhoneNumber.createNullable(validPhone); + expect(result.isSuccess).toBe(true); + expect(result.data.isSome()).toBe(true); + }); + + test("debe obtener el valor del número de teléfono", () => { + const result = PhoneNumber.create(validPhone); + expect(result.isSuccess).toBe(true); + const phoneNumber = result.data; + expect(phoneNumber?.getValue()).toBe(validPhone); + }); + + test("debe obtener el código de país del número", () => { + const result = PhoneNumber.create(validPhone); + expect(result.isSuccess).toBe(true); + const phoneNumber = result.data; + expect(phoneNumber?.getCountryCode()).toBe("US"); + }); + + test("debe obtener el número nacional del teléfono", () => { + const result = PhoneNumber.create(validPhone); + expect(result.isSuccess).toBe(true); + const phoneNumber = result.data; + expect(phoneNumber?.getNationalNumber()).toBe("4155552671"); + }); + + test("debe obtener el número formateado", () => { + const result = PhoneNumber.create(validPhone); + expect(result.isSuccess).toBe(true); + const phoneNumber = result.data; + expect(phoneNumber?.getNumber()).toBe(parsePhoneNumberWithError(validPhone).number.toString()); + }); + + test("debe devolver undefined para la extensión si no hay una", () => { + const result = PhoneNumber.create(validPhone); + expect(result.isSuccess).toBe(true); + const phoneNumber = result.data; + expect(phoneNumber?.getExtension()).toBeUndefined(); + }); +}); diff --git a/packages/rdx-ddd-domain/src/value-objects/__tests__/postal-address.test.ts b/packages/rdx-ddd-domain/src/value-objects/__tests__/postal-address.test.ts new file mode 100644 index 0000000..2b56103 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/__tests__/postal-address.test.ts @@ -0,0 +1,64 @@ +import { PostalAddress } from "../postal-address"; + +describe("PostalAddress Value Object", () => { + const validAddress = { + street: "123 Main St", + city: "Springfield", + postalCode: "12345", + state: "IL", + country: "USA", + }; + + test("✅ Debería crear un PostalAddress con valores válidos", () => { + const result = PostalAddress.create(validAddress); + + expect(result.isSuccess).toBe(true); + expect(result.data).toBeInstanceOf(PostalAddress); + }); + + test("❌ Debería fallar al crear un PostalAddress con código postal inválido", () => { + const invalidAddress = { ...validAddress, postalCode: "abc" }; // Código postal inválido + const result = PostalAddress.create(invalidAddress); + + expect(result.isFailure).toBe(true); + expect(result.error?.message).toBe("Invalid postal code format"); + }); + + test("✅ `createNullable` debería devolver Maybe.none si los valores son nulos o vacíos", () => { + expect(PostalAddress.createNullable().data.isSome()).toBe(false); + expect( + PostalAddress.createNullable({ + street: "", + city: "", + postalCode: "", + state: "", + country: "", + }).data.isSome() + ).toBe(false); + }); + + test("✅ `createNullable` debería devolver Maybe.some si los valores son válidos", () => { + const result = PostalAddress.createNullable(validAddress); + + expect(result.isSuccess).toBe(true); + expect(result.data.isSome()).toBe(true); + expect(result.data.unwrap()).toBeInstanceOf(PostalAddress); + }); + + test("✅ Métodos getters deberían devolver valores esperados", () => { + const address = PostalAddress.create(validAddress).data; + + expect(address.street).toBe(validAddress.street); + expect(address.city).toBe(validAddress.city); + expect(address.postalCode).toBe(validAddress.postalCode); + expect(address.state).toBe(validAddress.state); + expect(address.country).toBe(validAddress.country); + }); + + test("✅ `toString()` debería devolver la representación correcta", () => { + const address = PostalAddress.create(validAddress).data; + const expectedString = `${validAddress.street}, ${validAddress.city}, ${validAddress.postalCode}, ${validAddress.state}, ${validAddress.country}`; + + expect(address.toString()).toBe(expectedString); + }); +}); diff --git a/packages/rdx-ddd-domain/src/value-objects/__tests__/quantity.spec.ts b/packages/rdx-ddd-domain/src/value-objects/__tests__/quantity.spec.ts new file mode 100644 index 0000000..ad5409b --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/__tests__/quantity.spec.ts @@ -0,0 +1,112 @@ +import { Quantity } from "../quantity"; + +describe("Quantity", () => { + describe("create", () => { + it("debería crear una cantidad válida", () => { + const result = Quantity.create({ amount: 100, scale: 2 }); + expect(result.isSuccess).toBe(true); + expect(result.data.amount).toBe(100); + expect(result.data.scale).toBe(2); + }); + + it("debería fallar si la escala es negativa", () => { + const result = Quantity.create({ amount: 100, scale: -1 }); + expect(result.isFailure).toBe(true); + }); + }); + + describe("toNumber & toString", () => { + it("debería convertir correctamente a número", () => { + const quantity = Quantity.create({ amount: 150, scale: 2 }).data; + expect(quantity.toNumber()).toBe(1.5); + }); + + it("debería convertir correctamente a string", () => { + const quantity = Quantity.create({ amount: 123, scale: 2 }).data; + expect(quantity.toString()).toBe("1.23"); + }); + }); + + describe("increment", () => { + it("debería incrementar en 1 si no se pasa otra cantidad", () => { + const quantity = Quantity.create({ amount: 100, scale: 2 }).data; + const incremented = quantity.increment().data; + expect(incremented.amount).toBe(101); + expect(incremented.scale).toBe(2); + }); + + it("debería sumar correctamente si tienen la misma escala", () => { + const quantity1 = Quantity.create({ amount: 100, scale: 2 }).data; + const quantity2 = Quantity.create({ amount: 50, scale: 2 }).data; + + const result = quantity1.increment(quantity2); + expect(result.isSuccess).toBe(true); + expect(result.data.amount).toBe(150); + }); + + it("debería fallar si las escalas son diferentes", () => { + const quantity1 = Quantity.create({ amount: 100, scale: 2 }).data; + const quantity2 = Quantity.create({ amount: 50, scale: 1 }).data; + + const result = quantity1.increment(quantity2); + expect(result.isFailure).toBe(true); + }); + }); + + describe("decrement", () => { + it("debería decrementar en 1 si no se pasa otra cantidad", () => { + const quantity = Quantity.create({ amount: 100, scale: 2 }).data; + const decremented = quantity.decrement().data; + expect(decremented.amount).toBe(99); + }); + + it("debería restar correctamente si tienen la misma escala", () => { + const quantity1 = Quantity.create({ amount: 100, scale: 2 }).data; + const quantity2 = Quantity.create({ amount: 50, scale: 2 }).data; + + const result = quantity1.decrement(quantity2); + expect(result.isSuccess).toBe(true); + expect(result.data.amount).toBe(50); + }); + + it("debería fallar si las escalas son diferentes", () => { + const quantity1 = Quantity.create({ amount: 100, scale: 2 }).data; + const quantity2 = Quantity.create({ amount: 50, scale: 1 }).data; + + const result = quantity1.decrement(quantity2); + expect(result.isFailure).toBe(true); + }); + }); + + describe("convertScale", () => { + it("debería convertir correctamente a una nueva escala", () => { + const quantity = Quantity.create({ amount: 100, scale: 2 }).data; + const result = quantity.convertScale(1); + expect(result.isSuccess).toBe(true); + expect(result.data.amount).toBe(10); + expect(result.data.scale).toBe(1); + }); + + it("debería fallar si la escala está fuera de rango", () => { + const quantity = Quantity.create({ amount: 100, scale: 2 }).data; + const result = quantity.convertScale(3); + expect(result.isFailure).toBe(true); + }); + }); + + describe("hasSameScale", () => { + it("debería retornar true si las escalas son iguales", () => { + const quantity1 = Quantity.create({ amount: 100, scale: 2 }).data; + const quantity2 = Quantity.create({ amount: 50, scale: 2 }).data; + + expect(quantity1.hasSameScale(quantity2)).toBe(true); + }); + + it("debería retornar false si las escalas son diferentes", () => { + const quantity1 = Quantity.create({ amount: 100, scale: 2 }).data; + const quantity2 = Quantity.create({ amount: 50, scale: 1 }).data; + + expect(quantity1.hasSameScale(quantity2)).toBe(false); + }); + }); +}); diff --git a/packages/rdx-ddd-domain/src/value-objects/__tests__/slug.spec.ts b/packages/rdx-ddd-domain/src/value-objects/__tests__/slug.spec.ts new file mode 100644 index 0000000..f0a0e19 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/__tests__/slug.spec.ts @@ -0,0 +1,41 @@ +import { Slug } from "../slug"; // Ajusta la ruta según corresponda + +describe("Slug Value Object", () => { + test("Debe crear un Slug válido", () => { + const slugResult = Slug.create("valid-slug-123"); + expect(slugResult.isSuccess).toBe(true); + expect(slugResult.data.getValue()).toBe("valid-slug-123"); + }); + + test("Debe fallar si el Slug contiene caracteres inválidos", () => { + const slugResult = Slug.create("Invalid_Slug!"); + expect(slugResult.isSuccess).toBe(false); + expect(slugResult.error).toBeInstanceOf(Error); + }); + + test("Debe fallar si el Slug tiene menos de 2 caracteres", () => { + const slugResult = Slug.create("a"); + expect(slugResult.isSuccess).toBe(false); + expect(slugResult.error).toBeInstanceOf(Error); + }); + + test("Debe fallar si el Slug tiene más de 100 caracteres", () => { + const longSlug = "a".repeat(101); + const slugResult = Slug.create(longSlug); + expect(slugResult.isSuccess).toBe(false); + expect(slugResult.error).toBeInstanceOf(Error); + }); + + test("Debe permitir un Slug nullable vacío", () => { + const nullableSlugResult = Slug.createNullable(""); + expect(nullableSlugResult.isSuccess).toBe(true); + expect(nullableSlugResult.data.isSome()).toBe(false); + }); + + test("Debe permitir un Slug nullable con un valor válido", () => { + const nullableSlugResult = Slug.createNullable("my-slug"); + expect(nullableSlugResult.isSuccess).toBe(true); + expect(nullableSlugResult.data.isSome()).toBe(true); + expect(nullableSlugResult.data.getOrUndefined()?.toString()).toBe("my-slug"); + }); +}); diff --git a/packages/rdx-ddd-domain/src/value-objects/__tests__/tin-number.test.ts b/packages/rdx-ddd-domain/src/value-objects/__tests__/tin-number.test.ts new file mode 100644 index 0000000..4a63d29 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/__tests__/tin-number.test.ts @@ -0,0 +1,40 @@ +import { TINNumber } from "../tin-number"; + +describe("TINNumber", () => { + it("debería crear un TINNumber válido", () => { + const result = TINNumber.create("12345"); + expect(result.isSuccess).toBe(true); + expect(result.data.getValue()).toBe("12345"); + }); + + it("debería fallar si el valor es demasiado corto", () => { + const result = TINNumber.create("1"); + expect(result.isFailure).toBe(true); + expect(result.error?.message).toBe("TIN must be at least 2 characters long"); + }); + + it("debería fallar si el valor es demasiado largo", () => { + const result = TINNumber.create("12345678901"); + expect(result.isFailure).toBe(true); + expect(result.error?.message).toBe("TIN must be at most 10 characters long"); + }); + + it("debería devolver None cuando el valor es nulo o vacío en createNullable", () => { + const result = TINNumber.createNullable(""); + expect(result.isSuccess).toBe(true); + expect(result.data.isNone()).toBe(true); + }); + + it("debería devolver Some cuando el valor es válido en createNullable", () => { + const result = TINNumber.createNullable("6789"); + expect(result.isSuccess).toBe(true); + expect(result.data.isSome()).toBe(true); + expect(result.data.unwrap()?.toString()).toBe("6789"); + }); + + it("debería devolver el valor correcto en toString()", () => { + const result = TINNumber.create("ABC123"); + expect(result.isSuccess).toBe(true); + expect(result.data.toString()).toBe("ABC123"); + }); +}); diff --git a/packages/rdx-ddd-domain/src/value-objects/__tests__/unique-id.test.ts b/packages/rdx-ddd-domain/src/value-objects/__tests__/unique-id.test.ts new file mode 100644 index 0000000..2b594d2 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/__tests__/unique-id.test.ts @@ -0,0 +1,59 @@ +import { UniqueID } from "../unique-id"; + +// Mock UUID generation to ensure predictable tests +jest.mock("uuid", () => ({ v4: () => "123e4567-e89b-12d3-a456-426614174000" })); + +describe("UniqueID", () => { + test("should create a UniqueID with a valid UUID", () => { + const id = "123e4567-e89b-12d3-a456-426614174000"; + const result = UniqueID.create(id); + + expect(result.isSuccess).toBe(true); + expect(result.data.toString()).toBe(id); + }); + + test("should generate a UniqueID with a valid UUID", () => { + const result = UniqueID.generate(); + + expect(result.isSuccess).toBe(true); + expect(result.data.toString()).toBeTruthy(); + }); + + test("should fail to create UniqueID with an invalid UUID", () => { + const result = UniqueID.create("invalid-uuid"); + + expect(result.isFailure).toBe(true); + }); + + test("should fail when id is undefined and generateOnEmpty is false", () => { + const result = UniqueID.create(undefined, false); + + expect(result.isFailure).toBe(true); + }); + + test("should generate a new UUID when id is undefined and generateOnEmpty is true", () => { + const result = UniqueID.create(undefined, true); + + expect(result.isSuccess).toBe(true); + expect(result.data?.toString()).toBeTruthy(); + }); + + test("should fail when id is null", () => { + const result = UniqueID.create(null as any); + + expect(result.isFailure).toBe(true); + }); + + test("should create a UniqueID when id is an empty string and generateOnEmpty is true", () => { + const result = UniqueID.create(" ", true); + + expect(result.isSuccess).toBe(true); + expect(result.data?.toString()).toBeTruthy(); + }); + + test("should fail when id is an empty string and generateOnEmpty is false", () => { + const result = UniqueID.create(" ", false); + + expect(result.isFailure).toBe(true); + }); +}); diff --git a/packages/rdx-ddd-domain/src/value-objects/__tests__/utc-date.test.ts b/packages/rdx-ddd-domain/src/value-objects/__tests__/utc-date.test.ts new file mode 100644 index 0000000..9713a94 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/__tests__/utc-date.test.ts @@ -0,0 +1,41 @@ +import { UtcDate } from "../utc-date"; + +describe("UtcDate Value Object con Zod y Result", () => { + test("Debe crear una instancia con fecha y hora en UTC", () => { + const result = UtcDate.create("2025-01-06T19:36:18Z"); + expect(result.isSuccess).toBe(true); + expect(result.data?.toISOString()).toBe("2025-01-06T19:36:18.000Z"); + expect(result.data?.toDateString()).toBe("2025-01-06"); + }); + + test("Debe crear una instancia con solo fecha (sin hora)", () => { + const result = UtcDate.create("2020-11-12"); + expect(result.isSuccess).toBe(true); + expect(result.data?.toISOString()).toBe("2020-11-12T00:00:00.000Z"); // Normalizado con 00:00 UTC + expect(result.data?.toDateString()).toBe("2020-11-12"); + }); + + test("Debe devolver un Result.fail para formatos inválidos", () => { + expect(UtcDate.create("2020-07-32").isFailure).toBe(true); + expect(UtcDate.create("invalid-date").isFailure).toBe(true); + expect(UtcDate.create("2020/11/12").isFailure).toBe(true); + }); + + test("Debe comparar correctamente dos fechas idénticas", () => { + const date1 = UtcDate.create("2023-12-31T23:59:59Z").data!; + const date2 = UtcDate.create("2023-12-31T23:59:59Z").data!; + expect(date1.equals(date2)).toBe(true); + }); + + test("Debe comparar correctamente dos fechas diferentes", () => { + const date1 = UtcDate.create("2023-12-31T23:59:59Z").data!; + const date2 = UtcDate.create("2023-12-30T23:59:59Z").data!; + expect(date1.equals(date2)).toBe(false); + }); + + test("Debe manejar fechas sin hora correctamente en equals", () => { + const date1 = UtcDate.create("2020-11-12").data!; + const date2 = UtcDate.create("2020-11-12T00:00:00Z").data!; + expect(date1.equals(date2)).toBe(true); + }); +}); diff --git a/packages/rdx-ddd-domain/src/value-objects/__tests__/value-objects.test.ts b/packages/rdx-ddd-domain/src/value-objects/__tests__/value-objects.test.ts new file mode 100644 index 0000000..9ec436b --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/__tests__/value-objects.test.ts @@ -0,0 +1,47 @@ +import { ValueObject } from "../value-object"; + +interface ITestValueProps { + value: string; +} + +class TestValueObject extends ValueObject { + constructor(value: string) { + super({ value }); + } + + getValue() { + return this.props; + } +} + +describe("ValueObject", () => { + test("debe considerar dos ValueObjects con los mismos valores como iguales", () => { + const vo1 = new TestValueObject("test"); + const vo2 = new TestValueObject("test"); + expect(vo1.equals(vo2)).toBe(true); + }); + + test("debe considerar dos ValueObjects con valores diferentes como distintos", () => { + const vo1 = new TestValueObject("test1"); + const vo2 = new TestValueObject("test2"); + expect(vo1.equals(vo2)).toBe(false); + }); + + test("debe devolver false si el objeto comparado no es una instancia de ValueObject", () => { + const vo1 = new TestValueObject("test"); + expect(vo1.equals({ prop: "test" } as any)).toBe(false); + }); + + test("debe devolver false si el objeto comparado es null o undefined", () => { + const vo1 = new TestValueObject("test"); + expect(vo1.equals(null as any)).toBe(false); + expect(vo1.equals(undefined as any)).toBe(false); + }); + + test("debe garantizar la inmutabilidad de las propiedades", () => { + const vo = new TestValueObject("immutable"); + expect(() => { + (vo as any).props.prop = "mutated"; + }).toThrow(); + }); +}); diff --git a/packages/rdx-ddd-domain/src/value-objects/email-address.ts b/packages/rdx-ddd-domain/src/value-objects/email-address.ts new file mode 100644 index 0000000..9de02b7 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/email-address.ts @@ -0,0 +1,55 @@ +import { Result, Maybe } from "@rdx/utils"; +import { z } from "zod"; +import { ValueObject } from "./value-object"; + +interface EmailAddressProps { + value: string; +} + +export class EmailAddress extends ValueObject { + static create(value: string): Result { + const valueIsValid = EmailAddress.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.errors[0]?.message)); + } + return Result.ok(new EmailAddress({ value: valueIsValid.data! })); + } + + static createNullable(value?: string): Result, Error> { + if (!value || value.trim() === "") { + return Result.ok(Maybe.none()); + } + + return EmailAddress.create(value!).map((value) => Maybe.some(value)); + } + + private static validate(value: string) { + const schema = z.string().email({ message: "Invalid email format" }); + return schema.safeParse(value); + } + + getLocalPart(): string { + return this.props.value.split("@")[0] ?? ""; + } + + getDomain(): string { + return this.props.value.split("@")[1] ?? ""; + } + + getDomainExtension(): string { + return this.getDomain().split(".")[1] ?? ""; + } + + getDomainName(): string { + return this.getDomain().split(".")[0] ?? ""; + } + + getValue(): string { + return this.props.value; + } + + toPrimitive() { + return this.getValue(); + } +} diff --git a/packages/rdx-ddd-domain/src/value-objects/index.ts b/packages/rdx-ddd-domain/src/value-objects/index.ts new file mode 100644 index 0000000..5d341ab --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/index.ts @@ -0,0 +1,12 @@ +export * from "./email-address"; +export * from "./money-value"; +export * from "./name"; +export * from "./percentage"; +export * from "./phone-number"; +export * from "./postal-address"; +export * from "./quantity"; +export * from "./slug"; +export * from "./tin-number"; +export * from "./unique-id"; +export * from "./utc-date"; +export * from "./value-object"; diff --git a/packages/rdx-ddd-domain/src/value-objects/money-value.ts b/packages/rdx-ddd-domain/src/value-objects/money-value.ts new file mode 100644 index 0000000..a86bb44 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/money-value.ts @@ -0,0 +1,216 @@ +import { Result } from "@rdx/utils"; +import DineroFactory, { Currency, Dinero } from "dinero.js"; +import { Percentage } from "./percentage"; +import { Quantity } from "./quantity"; +import { ValueObject } from "./value-object"; + +const DEFAULT_SCALE = 2; +const DEFAULT_CURRENCY_CODE = "EUR"; + +type CurrencyData = Currency; + +export type RoundingMode = + | "HALF_ODD" + | "HALF_EVEN" + | "HALF_UP" + | "HALF_DOWN" + | "HALF_TOWARDS_ZERO" + | "HALF_AWAY_FROM_ZERO" + | "DOWN"; + +interface IMoneyValueProps { + amount: number; + scale?: number; + currency_code?: string; +} + +interface IMoneyValue { + amount: number; + scale: number; + currency: Dinero.Currency; + + getValue(): IMoneyValueProps; + convertScale(newScale: number): MoneyValue; + add(addend: MoneyValue): MoneyValue; + subtract(subtrahend: MoneyValue): MoneyValue; + multiply(multiplier: number | Quantity, roundingMode?: RoundingMode): MoneyValue; + divide(divisor: number, roundingMode?: RoundingMode): MoneyValue; + percentage(percentage: number, roundingMode?: RoundingMode): MoneyValue; + equalsTo(comparator: MoneyValue): boolean; + greaterThan(comparator: MoneyValue): boolean; + lessThan(comparator: MoneyValue): boolean; + isZero(): boolean; + isPositive(): boolean; + isNegative(): boolean; + hasSameCurrency(comparator: MoneyValue): boolean; + hasSameAmount(comparator: MoneyValue): boolean; + format(locale: string): string; +} + +export class MoneyValue extends ValueObject implements IMoneyValue { + private readonly dinero: Dinero; + + static create(props: IMoneyValueProps) { + return Result.ok(new MoneyValue(props)); + } + + constructor(props: IMoneyValueProps) { + super(props); + const { amount, scale, currency_code } = props; + this.dinero = Object.freeze( + DineroFactory({ + amount, + precision: scale || DEFAULT_SCALE, + currency: (currency_code as Currency) || DEFAULT_CURRENCY_CODE, + }) + ); // 🔒 Garantiza inmutabilidad + } + + get amount(): number { + return this.dinero.getAmount() / Math.pow(10, this.dinero.getPrecision()); + } + + get currency(): CurrencyData { + return this.dinero.getCurrency(); + } + + get scale(): number { + return this.dinero.getPrecision(); + } + + getValue(): IMoneyValueProps { + return this.props; + } + + /** Serializa el VO a una cadena del tipo "EUR:123400:2" */ + toPersistence(): string { + return `${this.currency}:${this.dinero.getAmount()}:${this.scale}`; + } + + /** Reconstruye el VO desde la cadena persistida */ + static fromPersistence(value: string): MoneyValue { + const [currencyCode, amountStr, scaleStr] = value.split(":"); + const amount = parseInt(amountStr ?? "", 10); + const scale = parseInt(scaleStr ?? "", 10); + const currency = currencyCode; + return new MoneyValue({ amount, scale, currency_code: currency }); + } + + toPrimitive() { + return { + amount: this.amount, + scale: this.scale, + currency_code: this.currency, + }; + } + + convertScale(newScale: number, roundingMode: RoundingMode = "HALF_UP"): MoneyValue { + const _newDinero = this.dinero.convertPrecision(newScale, roundingMode); + return new MoneyValue({ + amount: _newDinero.getAmount(), + scale: _newDinero.getPrecision(), + currency_code: _newDinero.getCurrency(), + }); + } + + add(addend: MoneyValue): MoneyValue { + return new MoneyValue({ + amount: this.dinero.add(addend.dinero).getAmount(), + scale: this.scale, + currency_code: this.currency, + }); + } + + subtract(subtrahend: MoneyValue): MoneyValue { + return new MoneyValue({ + amount: this.dinero.subtract(subtrahend.dinero).getAmount(), + scale: this.scale, + currency_code: this.currency, + }); + } + + multiply(multiplier: number | Quantity, roundingMode?: RoundingMode): MoneyValue { + const _multiplier = typeof multiplier === "number" ? multiplier : multiplier.toNumber(); + + const _newDinero = this.dinero.multiply(_multiplier, roundingMode); + return new MoneyValue({ + amount: _newDinero.getAmount(), + scale: _newDinero.getPrecision(), + currency_code: _newDinero.getCurrency(), + }); + } + + divide(divisor: number | Quantity, roundingMode?: RoundingMode): MoneyValue { + const _divisor = typeof divisor === "number" ? divisor : divisor.toNumber(); + + const _newDinero = this.dinero.divide(_divisor, roundingMode); + return new MoneyValue({ + amount: _newDinero.getAmount(), + scale: _newDinero.getPrecision(), + currency_code: _newDinero.getCurrency(), + }); + } + + percentage(percentage: number | Percentage, roundingMode?: RoundingMode): MoneyValue { + const _percentage = typeof percentage === "number" ? percentage : percentage.toNumber(); + + const _newDinero = this.dinero.percentage(_percentage, roundingMode); + return new MoneyValue({ + amount: _newDinero.getAmount(), + scale: _newDinero.getPrecision(), + currency_code: _newDinero.getCurrency(), + }); + } + + equalsTo(comparator: MoneyValue): boolean { + return this.dinero.equalsTo(comparator.dinero); + } + + greaterThan(comparator: MoneyValue): boolean { + return this.dinero.greaterThan(comparator.dinero); + } + + lessThan(comparator: MoneyValue): boolean { + return this.dinero.lessThan(comparator.dinero); + } + + isZero(): boolean { + return this.dinero.isZero(); + } + + isPositive(): boolean { + return this.amount > 0; + } + + isNegative(): boolean { + return this.amount < 0; + } + + hasSameCurrency(comparator: MoneyValue): boolean { + return this.dinero.hasSameCurrency(comparator.dinero); + } + + hasSameAmount(comparator: MoneyValue): boolean { + return this.dinero.hasSameAmount(comparator.dinero); + } + + /** + * Devuelve una cadena con el importe formateado. + * Ejemplo: 123456 -> €1,234.56 + * @param locale Código de idioma y país (ej. "es-ES") + * @returns Importe formateado + */ + format(locale: string): string { + const amount = this.amount; + const currency = this.currency; + const scale = this.scale; + + return new Intl.NumberFormat(locale, { + style: "currency", + currency: currency, + minimumFractionDigits: scale, + maximumFractionDigits: scale, + useGrouping: true, + }).format(amount); + } +} diff --git a/packages/rdx-ddd-domain/src/value-objects/name.ts b/packages/rdx-ddd-domain/src/value-objects/name.ts new file mode 100644 index 0000000..bd2ab17 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/name.ts @@ -0,0 +1,62 @@ +import { Maybe, Result } from "@rdx/utils"; +import { z } from "zod"; +import { ValueObject } from "./value-object"; + +interface INameProps { + value: string; +} + +export class Name extends ValueObject { + private static readonly MAX_LENGTH = 255; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .max(Name.MAX_LENGTH, { message: `Name must be at most ${Name.MAX_LENGTH} characters long` }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = Name.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.errors[0]?.message)); + } + return Result.ok(new Name({ value })); + } + + static createNullable(value?: string): Result, Error> { + if (!value || value.trim() === "") { + return Result.ok(Maybe.none()); + } + + return Name.create(value!).map((value) => Maybe.some(value)); + } + + static generateAcronym(name: string): string { + const words = name.split(" ").map((word) => word[0]?.toUpperCase()); + let acronym = words.join(""); + + // Asegurarse de que tenga 4 caracteres, recortando o añadiendo letras + if (acronym.length > 4) { + acronym = acronym.slice(0, 4); + } else if (acronym.length < 4) { + acronym = acronym.padEnd(4, "X"); // Se completa con 'X' si es necesario + } + + return acronym; + } + + getAcronym(): string { + return Name.generateAcronym(this.toString()); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive() { + return this.getValue(); + } +} diff --git a/packages/rdx-ddd-domain/src/value-objects/percentage.ts b/packages/rdx-ddd-domain/src/value-objects/percentage.ts new file mode 100644 index 0000000..a69f5d6 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/percentage.ts @@ -0,0 +1,87 @@ +import { Result } from "@rdx/utils"; +import { z } from "zod"; +import { ValueObject } from "./value-object"; + +const DEFAULT_SCALE = 2; + +interface IPercentageProps { + amount: number; + scale: number; +} + +interface IPercentage { + amount: number; + scale: number; + + getValue(): IPercentageProps; + toNumber(): number; + toString(): string; +} + +export class Percentage extends ValueObject implements IPercentage { + public static readonly DEFAULT_SCALE = DEFAULT_SCALE; + public static readonly MIN_VALUE = 0; + public static readonly MAX_VALUE = 100; + + public static readonly MIN_SCALE = 0; + public static readonly MAX_SCALE = 2; + + protected static validate(values: IPercentageProps) { + const schema = z.object({ + amount: z.number().int().min(Percentage.MIN_VALUE, "La cantidad no puede ser negativa."), + scale: z + .number() + .int() + .min(Percentage.MIN_SCALE) + .max( + Percentage.MAX_SCALE, + `La escala debe estar entre ${Percentage.MIN_SCALE} y ${Percentage.MAX_SCALE}.` + ), + }); + + return schema.safeParse(values); + } + + static create(props: { amount: number; scale?: number }): Result { + const { amount, scale = Percentage.DEFAULT_SCALE } = props; + + const validationResult = Percentage.validate({ amount, scale }); + if (!validationResult.success) { + return Result.fail(new Error(validationResult.error.errors.map((e) => e.message).join(", "))); + } + + // Cálculo del valor real del porcentaje + const realValue = amount / Math.pow(10, scale); + + // Validación de rango + if (realValue > Percentage.MAX_VALUE) { + return Result.fail(new Error("El porcentaje no puede ser mayor a 100%.")); + } + + return Result.ok(new Percentage({ amount, scale })); + } + + get amount(): number { + return this.props.amount; + } + + get scale(): number { + return this.props.scale; + } + + getValue(): IPercentageProps { + return this.props; + } + + toPrimitive() { + return this.getValue(); + } + + toNumber(): number { + return this.amount / Math.pow(10, this.scale); + } + + toString(): string { + return `${this.toNumber().toFixed(this.scale)}%`; + } +} diff --git a/packages/rdx-ddd-domain/src/value-objects/phone-number.ts b/packages/rdx-ddd-domain/src/value-objects/phone-number.ts new file mode 100644 index 0000000..d71755a --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/phone-number.ts @@ -0,0 +1,62 @@ +import { Maybe, Result } from "@rdx/utils"; +import { isValidPhoneNumber, parsePhoneNumberWithError } from "libphonenumber-js"; +import { z } from "zod"; +import { ValueObject } from "./value-object"; + +interface PhoneNumberProps { + value: string; +} + +export class PhoneNumber extends ValueObject { + static create(value: string): Result { + const valueIsValid = PhoneNumber.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.errors[0]?.message)); + } + return Result.ok(new PhoneNumber({ value: valueIsValid.data! })); + } + + static createNullable(value?: string): Result, Error> { + if (!value || value.trim() === "") { + return Result.ok(Maybe.none()); + } + + return PhoneNumber.create(value!).map((value) => Maybe.some(value)); + } + + static validate(value: string) { + const schema = z + .string() + .refine( + isValidPhoneNumber, + "Please specify a valid phone number (include the international prefix)." + ) + .transform((value: string) => parsePhoneNumberWithError(value).number.toString()); + return schema.safeParse(value); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive(): string { + return this.getValue(); + } + + getCountryCode(): string | undefined { + return parsePhoneNumberWithError(this.props.value).country; + } + + getNationalNumber(): string { + return parsePhoneNumberWithError(this.props.value).nationalNumber; + } + + getNumber(): string { + return parsePhoneNumberWithError(this.props.value).number.toString(); + } + + getExtension(): string | undefined { + return parsePhoneNumberWithError(this.props.value).ext; + } +} diff --git a/packages/rdx-ddd-domain/src/value-objects/postal-address.ts b/packages/rdx-ddd-domain/src/value-objects/postal-address.ts new file mode 100644 index 0000000..3ebfc57 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/postal-address.ts @@ -0,0 +1,109 @@ +import { Maybe, Result } from "@rdx/utils"; +import { z } from "zod"; +import { ValueObject } from "./value-object"; + +// 📌 Validaciones usando `zod` +const postalCodeSchema = z + .string() + .min(4, "Invalid postal code format") + .max(10, "Invalid postal code format") + .regex(/^\d{4,10}$/, { + message: "Invalid postal code format", + }); + +const streetSchema = z.string().min(2).max(255); +const street2Schema = z.string().optional(); +const citySchema = z.string().min(2).max(50); +const stateSchema = z.string().min(2).max(50); +const countrySchema = z.string().min(2).max(56); + +interface IPostalAddressProps { + street: string; + street2?: string; + city: string; + postalCode: string; + state: string; + country: string; +} + +export class PostalAddress extends ValueObject { + protected static validate(values: IPostalAddressProps) { + return z + .object({ + street: streetSchema, + street2: street2Schema, + city: citySchema, + postalCode: postalCodeSchema, + state: stateSchema, + country: countrySchema, + }) + .safeParse(values); + } + + static create(values: IPostalAddressProps): Result { + const valueIsValid = PostalAddress.validate(values); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.errors[0]?.message)); + } + return Result.ok(new PostalAddress(values)); + } + + static createNullable(values?: IPostalAddressProps): Result, Error> { + if (!values || Object.values(values).every((value) => value.trim() === "")) { + return Result.ok(Maybe.none()); + } + + return PostalAddress.create(values!).map((value) => Maybe.some(value)); + } + + static update( + oldAddress: PostalAddress, + data: Partial + ): Result { + return PostalAddress.create({ + street: data.street ?? oldAddress.street, + street2: data.street2?.getOrUndefined() ?? oldAddress.street2.getOrUndefined(), + city: data.city ?? oldAddress.city, + postalCode: data.postalCode ?? oldAddress.postalCode, + state: data.state ?? oldAddress.state, + country: data.country ?? oldAddress.country, + }).getOrElse(this); + } + + get street(): string { + return this.props.street; + } + + get street2(): Maybe { + return Maybe.fromNullable(this.props.street2); + } + + get city(): string { + return this.props.city; + } + + get postalCode(): string { + return this.props.postalCode; + } + + get state(): string { + return this.props.state; + } + + get country(): string { + return this.props.country; + } + + getValue(): IPostalAddressProps { + return this.props; + } + + toPrimitive() { + return this.getValue(); + } + + toString(): string { + return `${this.props.street}, ${this.props.street2}, ${this.props.city}, ${this.props.postalCode}, ${this.props.state}, ${this.props.country}`; + } +} diff --git a/packages/rdx-ddd-domain/src/value-objects/quantity.ts b/packages/rdx-ddd-domain/src/value-objects/quantity.ts new file mode 100644 index 0000000..906aed6 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/quantity.ts @@ -0,0 +1,142 @@ +import { Result } from "@rdx/utils"; +import { z } from "zod"; +import { ValueObject } from "./value-object"; + +const DEFAULT_SCALE = 2; + +interface IQuantityProps { + amount: number; + scale: number; +} + +interface IQuantity { + amount: number; + scale: number; + + getValue(): IQuantityProps; + toNumber(): number; + toString(): string; + + isZero(): boolean; + isPositive(): boolean; + isNegative(): boolean; + + increment(anotherQuantity?: Quantity): Result; + decrement(anotherQuantity?: Quantity): Result; + hasSameScale(otherQuantity: Quantity): boolean; + convertScale(newScale: number): Result; +} + +export class Quantity extends ValueObject implements IQuantity { + protected static validate(values: IQuantityProps) { + const schema = z.object({ + amount: z.number().int(), + scale: z.number().int().min(Quantity.MIN_SCALE).max(Quantity.MAX_SCALE), + }); + + return schema.safeParse(values); + } + + public static readonly DEFAULT_SCALE = DEFAULT_SCALE; + public static readonly MIN_SCALE = 0; + public static readonly MAX_SCALE = 2; + + static create(props: IQuantityProps) { + const checkProps = Quantity.validate(props); + + if (!checkProps.success) { + return Result.fail(new Error(checkProps.error.errors[0].message)); + } + return Result.ok(new Quantity({ ...checkProps.data! })); + } + + get amount(): number { + return this.props.amount; + } + + get scale(): number { + return this.props.scale; + } + + getValue(): IQuantityProps { + return this.props; + } + + toPrimitive() { + return this.getValue(); + } + + toNumber(): number { + return this.amount / Math.pow(10, this.scale); + } + + toString(): string { + return this.toNumber().toFixed(this.scale); + } + + isZero(): boolean { + return this.amount === 0; + } + + isPositive(): boolean { + return this.amount > 0; + } + + isNegative(): boolean { + return this.amount < 0; + } + + increment(anotherQuantity?: Quantity): Result { + if (!anotherQuantity) { + return Quantity.create({ + amount: Number(this.amount) + 1, + scale: this.scale, + }); + } + + if (!this.hasSameScale(anotherQuantity)) { + return Result.fail(Error("No se pueden sumar cantidades con diferentes escalas.")); + } + + return Quantity.create({ + amount: Number(this.amount) + Number(anotherQuantity.amount), + scale: this.scale, + }); + } + + decrement(anotherQuantity?: Quantity): Result { + if (!anotherQuantity) { + return Quantity.create({ + amount: Number(this.amount) - 1, + scale: this.scale, + }); + } + + if (!this.hasSameScale(anotherQuantity)) { + return Result.fail(Error("No se pueden restar cantidades con diferentes escalas.")); + } + + return Quantity.create({ + amount: Number(this.amount) - Number(anotherQuantity.amount), + scale: this.scale, + }); + } + + hasSameScale(otherQuantity: Quantity): boolean { + return this.scale === otherQuantity.scale; + } + + convertScale(newScale: number): Result { + if (newScale < Quantity.MIN_SCALE || newScale > Quantity.MAX_SCALE) { + return Result.fail(new Error(`Scale out of range: ${newScale}`)); + } + + const oldFactor = Math.pow(10, this.scale); + const value = Number(this.amount) / oldFactor; + + const newFactor = Math.pow(10, newScale); + const newValue = Math.round(value * newFactor); + + return Quantity.create({ amount: newValue, scale: newScale }); + } +} diff --git a/packages/rdx-ddd-domain/src/value-objects/slug.ts b/packages/rdx-ddd-domain/src/value-objects/slug.ts new file mode 100644 index 0000000..355aa9a --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/slug.ts @@ -0,0 +1,49 @@ +import { Maybe, Result } from "@rdx/utils"; +import { z } from "zod"; +import { ValueObject } from "./value-object"; + +interface SlugProps { + value: string; +} + +export class Slug extends ValueObject { + protected static readonly MIN_LENGTH = 2; + protected static readonly MAX_LENGTH = 100; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .regex(/^[a-z0-9-]+$/, { + message: "Slug must contain only lowercase letters, numbers, and hyphens", + }) + .min(Slug.MIN_LENGTH, { message: `Slug must be at least ${Slug.MIN_LENGTH} characters long` }) + .max(Slug.MAX_LENGTH, { message: `Slug must be at most ${Slug.MAX_LENGTH} characters long` }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = Slug.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.errors[0].message)); + } + return Result.ok(new Slug({ value: valueIsValid.data! })); + } + + static createNullable(value?: string): Result, Error> { + if (!value || value.trim() === "") { + return Result.ok(Maybe.none()); + } + + return Slug.create(value!).map((value: Slug) => Maybe.some(value)); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive(): string { + return this.getValue(); + } +} diff --git a/packages/rdx-ddd-domain/src/value-objects/tin-number.ts b/packages/rdx-ddd-domain/src/value-objects/tin-number.ts new file mode 100644 index 0000000..6202915 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/tin-number.ts @@ -0,0 +1,51 @@ +import { Maybe, Result } from "@rdx/utils"; +import { z } from "zod"; +import { ValueObject } from "./value-object"; + +interface TINNumberProps { + value: string; +} + +export class TINNumber extends ValueObject { + private static readonly MIN_LENGTH = 2; + private static readonly MAX_LENGTH = 10; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .min(TINNumber.MIN_LENGTH, { + message: `TIN must be at least ${TINNumber.MIN_LENGTH} characters long`, + }) + .max(TINNumber.MAX_LENGTH, { + message: `TIN must be at most ${TINNumber.MAX_LENGTH} characters long`, + }); + + return schema.safeParse(value); + } + + static create(value: string): Result { + const valueIsValid = TINNumber.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.errors[0]?.message)); + } + return Result.ok(new TINNumber({ value: valueIsValid.data! })); + } + + static createNullable(value?: string): Result, Error> { + if (!value || value.trim() === "") { + return Result.ok(Maybe.none()); + } + + return TINNumber.create(value!).map((value) => Maybe.some(value)); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive(): string { + return this.props.value; + } +} diff --git a/packages/rdx-ddd-domain/src/value-objects/unique-id.ts b/packages/rdx-ddd-domain/src/value-objects/unique-id.ts new file mode 100644 index 0000000..f730940 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/unique-id.ts @@ -0,0 +1,46 @@ +import { Result } from "@rdx/utils"; +import { v4 as uuidv4 } from "uuid"; +import { z } from "zod"; +import { ValueObject } from "./value-object"; + +export class UniqueID extends ValueObject { + static create(id?: string, generateOnEmpty: boolean = false): Result { + if (!id || id?.trim() === "") { + if (!generateOnEmpty) { + return Result.fail(new Error("ID cannot be undefined or null")); + } + return UniqueID.generateNewID(); + } + + const result = UniqueID.validate(id!); + + return result.success + ? Result.ok(new UniqueID(result.data)) + : Result.fail(new Error(result.error.errors[0]?.message)); + } + + static generate(): Result { + return Result.ok(new UniqueID(uuidv4())); + } + + static validate(id: string) { + const schema = z.string().trim().uuid({ message: "Invalid UUID format" }); + return schema.safeParse(id); + } + + static generateNewID(): Result { + return Result.ok(new UniqueID(uuidv4())); + } + + getValue(): string { + return this.props; + } + + toString(): string { + return this.getValue(); + } + + toPrimitive() { + return this.getValue(); + } +} diff --git a/packages/rdx-ddd-domain/src/value-objects/utc-date.ts b/packages/rdx-ddd-domain/src/value-objects/utc-date.ts new file mode 100644 index 0000000..0832d33 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/utc-date.ts @@ -0,0 +1,74 @@ +import { Result } from "@rdx/utils"; +import { z } from "zod"; +import { ValueObject } from "./value-object"; + +interface IUtcDateProps { + value: string; +} + +export class UtcDate extends ValueObject { + private readonly date!: Date; + + private constructor(props: IUtcDateProps) { + super(props); + const { value: dateString } = props; + this.date = Object.freeze(new Date(dateString)); + } + + static validate(dateString: string) { + const dateStr = z.union([ + z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/, "Invalid ISO 8601 format"), + z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Invalid YYYY-MM-DD format"), + ]); + + const dateStrToDate = dateStr.pipe(z.coerce.date()); + + return dateStrToDate.safeParse(dateString); + } + + /** + * Crea una instancia de UtcDate a partir de un string en formato UTC ISO 8601. + * @param dateString Fecha en formato UTC (con o sin hora YYYY-MM-DD) + * @returns UtcDate si es válida, Error en caso contrario. + */ + static create(dateString: string): Result { + const dateIsValid = UtcDate.validate(dateString); + if (!dateIsValid.success) { + return Result.fail(new Error(`Invalid UTC date format: ${dateString}`)); + } + + return Result.ok(new UtcDate({ value: dateString })); + } + + getValue(): string { + return this.props.value; + } + + /** + * Devuelve la fecha completa en formato UTC con hora (ISO 8601). Ejemplo: 2025-12-31T23:59:59Z. + */ + toPrimitive() { + return this.toISOString(); + } + + /** + * Devuelve la fecha en formato UTC sin hora (YYYY-MM-DD). + */ + toDateString(): string { + return this.date.toISOString().split("T")[0] ?? ""; + } + + /** + * Devuelve la fecha en formato UTC con hora (ISO 8601). Ejemplo: 2025-12-31T23:59:59Z. + */ + toISOString(): string { + return this.date.toISOString(); + } + + /** + * Compara si dos instancias de UtcDate son iguales. + */ + equals(other: UtcDate): boolean { + return this.toISOString() === other.toISOString(); + } +} diff --git a/packages/rdx-ddd-domain/src/value-objects/value-object.ts b/packages/rdx-ddd-domain/src/value-objects/value-object.ts new file mode 100644 index 0000000..d356a17 --- /dev/null +++ b/packages/rdx-ddd-domain/src/value-objects/value-object.ts @@ -0,0 +1,25 @@ +import { shallowEqual } from "shallow-equal-object"; + +export abstract class ValueObject { + protected readonly props: T; + + protected constructor(props: T) { + this.props = Object.freeze(props); // 🔒 Garantiza inmutabilidad + } + + abstract getValue(): any; + + abstract toPrimitive(): any; + + equals(other: ValueObject): boolean { + if (!(other instanceof ValueObject)) { + return false; + } + + if (other.props === undefined || other.props === null) { + return false; + } + + return shallowEqual(this.props, other.props); + } +} diff --git a/packages/rdx-ddd-domain/tsconfig.json b/packages/rdx-ddd-domain/tsconfig.json new file mode 100644 index 0000000..ae936c8 --- /dev/null +++ b/packages/rdx-ddd-domain/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["node"], + + "paths": { + "@/*": ["./src/*"] + } + }, + "files": ["src/index.ts"], + "include": ["src/index.ts"], + "exclude": ["node_modules", "dist", "**/*/__tests__"] +} diff --git a/packages/rdx-ddd-domain/turbo.json b/packages/rdx-ddd-domain/turbo.json new file mode 100644 index 0000000..52a7d7a --- /dev/null +++ b/packages/rdx-ddd-domain/turbo.json @@ -0,0 +1,9 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + } + } +} diff --git a/packages/rdx-logger/package.json b/packages/rdx-logger/package.json new file mode 100644 index 0000000..c94bdeb --- /dev/null +++ b/packages/rdx-logger/package.json @@ -0,0 +1,44 @@ +{ + "name": "@rdx/logger", + "version": "0.0.0", + "private": true, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist && rm -rf node_modules", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "jest": { + "preset": "@repo/jest-presets/node" + }, + "devDependencies": { + "@jest/globals": "^29.7.0", + "@repo/eslint-config": "workspace:*", + "@repo/jest-presets": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/dotenv": "^8.2.3", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.14", + "@types/node": "^22.15.2", + "@types/react": "^19.1.2", + "@types/winston": "^2.4.4", + "eslint": "^9.25.1", + "jest": "^29.7.0", + "typescript": "^5.8.3" + }, + "dependencies": { + "cls-rtracer": "^2.6.3", + "dotenv": "^16.5.0", + "path": "^0.12.7", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0" + } +} diff --git a/packages/rdx-logger/src/index.ts b/packages/rdx-logger/src/index.ts new file mode 100644 index 0000000..a3e24f1 --- /dev/null +++ b/packages/rdx-logger/src/index.ts @@ -0,0 +1,102 @@ +import rTracer from "cls-rtracer"; +import dotenv from "dotenv"; +import path from "path"; +import { createLogger, format, transports } from "winston"; +import DailyRotateFile from "winston-daily-rotate-file"; + +dotenv.config(); + +//const splatSymbol = Symbol.for("splat"); + +const initLogger = () => { + const isProduction = process.env.NODE_ENV === "production"; + + const consoleFormat = format.combine( + format.colorize(), + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + format.align(), + format.splat(), + format.errors({ stack: !isProduction }), + + format.printf((info) => { + const rid = rTracer.id(); + + if (typeof info.message === "object") { + info.message = JSON.stringify(info.message, null, 3); + } + + let out = + isProduction && rid + ? `${info.timestamp} [request-id:${rid}] - ${info.level}: [${info.label}]: ${info.message}` + : `${info.timestamp} - ${info.level}: [${info.label}]: ${info.message}`; + + /*if (info.metadata["error"]) { + out = `${out} ${info.metadata.error}`; + if (info.metadata?.error?.stack) { + out = `${out} ${info.metadata.error.stack}`; + } + }*/ + + return out; + }) + ); + + const fileFormat = format.combine( + format.timestamp(), + format.splat(), + format.label({ label: path.basename(String(require.main?.filename)) }), + //format.metadata(), + format.metadata({ fillExcept: ["message", "level", "timestamp", "label"] }), + format.simple(), + format.json() + ); + + const logger = createLogger( + isProduction + ? { + level: isProduction ? "error" : "debug", + + format: fileFormat, + + transports: [ + new DailyRotateFile({ + dirname: isProduction ? "/logs" : ".", + filename: "error-%DATE%.log", + datePattern: "YYYY-MM-DD", + utc: true, + level: "error", + maxSize: "5m", + maxFiles: "1d", + }), + new DailyRotateFile({ + dirname: isProduction ? "/logs" : ".", + filename: "debug-%DATE%.log", + datePattern: "YYYY-MM-DD", + utc: true, + level: "debug", + maxSize: "5m", + maxFiles: "1d", + }), + ], + } + : {} + ); + + // + // If we're not in production then log to the `console` with the format: + // `${info.level}: ${info.message} JSON.stringify({ ...rest }) ` + // + if (!isProduction) { + logger.add( + new transports.Console({ + format: consoleFormat, + level: "debug", + }) + ); + } + + return logger; +}; + +const logger = initLogger(); +export { logger }; diff --git a/packages/rdx-logger/tsconfig.json b/packages/rdx-logger/tsconfig.json new file mode 100644 index 0000000..ae936c8 --- /dev/null +++ b/packages/rdx-logger/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["node"], + + "paths": { + "@/*": ["./src/*"] + } + }, + "files": ["src/index.ts"], + "include": ["src/index.ts"], + "exclude": ["node_modules", "dist", "**/*/__tests__"] +} diff --git a/packages/rdx-logger/turbo.json b/packages/rdx-logger/turbo.json new file mode 100644 index 0000000..35be9c6 --- /dev/null +++ b/packages/rdx-logger/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/packages/rdx-modules/package.json b/packages/rdx-modules/package.json new file mode 100644 index 0000000..8da1f53 --- /dev/null +++ b/packages/rdx-modules/package.json @@ -0,0 +1,39 @@ +{ + "name": "@rdx/modules", + "version": "0.0.0", + "private": true, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist && rm -rf node_modules", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "jest": { + "preset": "@repo/jest-presets/node" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/jest-presets": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.14", + "@types/node": "^22.15.2", + "@types/react": "^19.1.2", + "jest": "^29.7.0", + "typescript": "^5.8.3" + }, + "dependencies": { + "express": "^4.21.2", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "sequelize": "^6.37.7" + } +} diff --git a/packages/rdx-modules/src/index.ts b/packages/rdx-modules/src/index.ts new file mode 100644 index 0000000..cb2c413 --- /dev/null +++ b/packages/rdx-modules/src/index.ts @@ -0,0 +1,3 @@ +export * from "./module-client.interface"; +export * from "./module-server.interface"; +export * from "./types"; diff --git a/packages/rdx-modules/src/module-client.interface.ts b/packages/rdx-modules/src/module-client.interface.ts new file mode 100644 index 0000000..12c1c9d --- /dev/null +++ b/packages/rdx-modules/src/module-client.interface.ts @@ -0,0 +1,16 @@ +// Contrato para los Modules frontend (React) + +import * as React from "react"; +import { ReactNode } from "react"; +import { ModuleMetadata } from "./types"; + +export interface ModuleClientMetadata extends ModuleMetadata { + route: string; + icon?: ReactNode; +} + +export interface IModuleClient { + metadata: ModuleClientMetadata; + component: React.FC; + setup?(): void; +} diff --git a/packages/rdx-modules/src/module-server.interface.ts b/packages/rdx-modules/src/module-server.interface.ts new file mode 100644 index 0000000..9c41ad5 --- /dev/null +++ b/packages/rdx-modules/src/module-server.interface.ts @@ -0,0 +1,10 @@ +//Contrato para los Modules backend (Node.js) + +import { Application } from "express"; +import { ModuleDependencies, ModuleMetadata } from "./types"; + +export interface IModuleServer { + metadata: ModuleMetadata; + init(app: Application): void; + registerDependencies?(): ModuleDependencies; +} diff --git a/packages/rdx-modules/src/types.ts b/packages/rdx-modules/src/types.ts new file mode 100644 index 0000000..2250fc9 --- /dev/null +++ b/packages/rdx-modules/src/types.ts @@ -0,0 +1,17 @@ +// Contiene tipos comunes entre cliente y servidor + +import { Sequelize } from "sequelize"; + +export type ModelInitializer = (sequelize: Sequelize) => any; + +export interface ModuleMetadata { + name: string; + version: string; + description?: string; + dependencies?: string[]; +} + +export interface ModuleDependencies { + models?: ModelInitializer[]; + services?: { [key: string]: any }; +} diff --git a/packages/rdx-modules/tsconfig.json b/packages/rdx-modules/tsconfig.json new file mode 100644 index 0000000..ae936c8 --- /dev/null +++ b/packages/rdx-modules/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["node"], + + "paths": { + "@/*": ["./src/*"] + } + }, + "files": ["src/index.ts"], + "include": ["src/index.ts"], + "exclude": ["node_modules", "dist", "**/*/__tests__"] +} diff --git a/packages/rdx-modules/turbo.json b/packages/rdx-modules/turbo.json new file mode 100644 index 0000000..35be9c6 --- /dev/null +++ b/packages/rdx-modules/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/packages/rdx-utils/package.json b/packages/rdx-utils/package.json new file mode 100644 index 0000000..c077568 --- /dev/null +++ b/packages/rdx-utils/package.json @@ -0,0 +1,29 @@ +{ + "name": "@rdx/utils", + "version": "0.0.0", + "private": true, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist && rm -rf node_modules", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "jest": { + "preset": "@repo/jest-presets/node" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/jest-presets": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/node": "^22.15.2", + "typescript": "^5.8.3" + } +} diff --git a/packages/rdx-utils/src/helpers/collection.ts b/packages/rdx-utils/src/helpers/collection.ts new file mode 100644 index 0000000..4c08253 --- /dev/null +++ b/packages/rdx-utils/src/helpers/collection.ts @@ -0,0 +1,124 @@ +/** + * Clase genérica para manejar una colección de elementos. + * Ofrece métodos básicos para manipular, consultar y recorrer los elementos. + */ +export class Collection { + private items: T[]; + private totalItems: number | null; + + /** + * Crea una nueva colección. + * @param items - Elementos iniciales de la colección. + * @param totalItems - Total de elementos esperados (opcional). Si no se define, se usa la longitud del array inicial. + */ + constructor(items: T[] = [], totalItems: number | null = null) { + this.items = [...items]; + this.totalItems = totalItems ?? items.length; + } + + /** + * Vacía la colección y reinicia el total de elementos. + */ + reset(): void { + this.items = []; + this.totalItems = 0; + } + + /** + * Agrega un nuevo elemento a la colección. + * @param item - Elemento a agregar. + */ + add(item: T): void { + this.items.push(item); + if (this.totalItems !== null) { + this.totalItems++; + } + } + + /** + * Elimina un elemento de la colección, si existe. + * @param item - Elemento a eliminar. + * @returns `true` si el elemento fue eliminado, `false` si no se encontró. + */ + remove(item: T): boolean { + const index = this.items.indexOf(item); + if (index !== -1) { + this.items.splice(index, 1); + if (this.totalItems !== null) { + this.totalItems--; + } + return true; + } + return false; + } + + /** + * Devuelve una copia de todos los elementos de la colección. + * @returns Array de elementos. + */ + getAll(): T[] { + return [...this.items]; + } + + /** + * Devuelve la cantidad actual de elementos en la colección. + * @returns Número de elementos presentes. + */ + size(): number { + return this.items.length; + } + + /** + * Devuelve el total de elementos esperados en la colección. + * Puede diferir de `size()` si los datos son paginados, por ejemplo. + * @returns Número total o `null` si no se especificó. + */ + total(): number | null { + return this.totalItems; + } + + /** + * Aplica una función a cada elemento y devuelve un nuevo array con los resultados. + * @param callback - Función transformadora. + * @returns Nuevo array con los elementos transformados. + */ + map(callback: (item: T, index: number, array: T[]) => U): U[] { + return this.items.map(callback); + } + + /** + * Devuelve un array con los elementos que cumplen la condición del callback. + * @param callback - Función de filtrado. + * @returns Nuevo array con los elementos filtrados. + */ + filter(callback: (item: T, index: number, array: T[]) => boolean): T[] { + return this.items.filter(callback); + } + + /** + * Devuelve el primer elemento que cumple la condición del callback. + * @param callback - Función de búsqueda. + * @returns El primer elemento que cumple la condición, o `undefined` si no hay coincidencias. + */ + find(callback: (item: T, index: number, array: T[]) => boolean): T | undefined { + return this.items.find(callback); + } + + /** + * Verifica si al menos un elemento cumple la condición dada. + * @param callback - Función de evaluación. + * @returns `true` si al menos un elemento cumple, `false` en caso contrario. + */ + some(callback: (item: T, index: number, array: T[]) => boolean): boolean { + return this.items.some(callback); + } + + /** + * Verifica si todos los elementos cumplen la condición dada. + * @param callback - Función de evaluación. + * @returns `true` si todos cumplen, `false` si alguno no. + */ + every(callback: (item: T, index: number, array: T[]) => boolean): boolean { + return this.items.every(callback); + } +} diff --git a/packages/rdx-utils/src/helpers/index.ts b/packages/rdx-utils/src/helpers/index.ts new file mode 100644 index 0000000..5d1da0b --- /dev/null +++ b/packages/rdx-utils/src/helpers/index.ts @@ -0,0 +1,4 @@ +export * from "./collection"; +export * from "./maybe"; +export * from "./result"; +export * from "./utils"; diff --git a/packages/rdx-utils/src/helpers/maybe.ts b/packages/rdx-utils/src/helpers/maybe.ts new file mode 100644 index 0000000..390c090 --- /dev/null +++ b/packages/rdx-utils/src/helpers/maybe.ts @@ -0,0 +1,46 @@ +/** + * Uso: + * + * const maybeNumber = Maybe.some(10); + * const doubled = maybeNumber.map(n => n * 2); + * console.log(doubled.getValue()); // 20 + + * const noValue = Maybe.none(); + * console.log(noValue.isSome()); // false + **/ + +export class Maybe { + private constructor(private readonly value?: T) {} + + static fromNullable(value?: T): Maybe { + return value === undefined || value === null ? Maybe.none() : Maybe.some(value); + } + + static some(value: T): Maybe { + return new Maybe(value); + } + + static none(): Maybe { + return new Maybe(); + } + + isSome(): boolean { + return this.value !== undefined; + } + + isNone(): boolean { + return !this.isSome(); + } + + unwrap(): T | undefined { + return this.value; + } + + getOrUndefined(): T | undefined { + return this.unwrap(); + } + + map(fn: (value: T) => U): Maybe { + return this.isSome() ? Maybe.some(fn(this.value as T)) : Maybe.none(); + } +} diff --git a/packages/rdx-utils/src/helpers/result.ts b/packages/rdx-utils/src/helpers/result.ts new file mode 100644 index 0000000..a0136f0 --- /dev/null +++ b/packages/rdx-utils/src/helpers/result.ts @@ -0,0 +1,92 @@ +export class Result { + private readonly _isSuccess: boolean; + private readonly _data?: T; + private readonly _error?: E; + + private constructor(props: { isSuccess: boolean; error?: E; data?: T }) { + const { isSuccess, error, data } = props; + if (isSuccess && error) { + throw new Error(`InvalidOperation: A result cannot be successful and contain an error`); + } + if (!isSuccess && !error) { + throw new Error(`InvalidOperation: A failing result needs to contain an error message`); + } + + this._isSuccess = isSuccess; + this._error = error; + this._data = data; + + Object.freeze(this); + } + + static ok(data?: T): Result { + return new Result({ isSuccess: true, data }); + } + + static fail(error?: E): Result { + return new Result({ isSuccess: false, error }); + } + + static combine(results: Result[]): Result { + for (const result of results) { + if (result.isFailure) { + return result; + } + } + + return Result.ok(); + } + + get isSuccess(): boolean { + return this._isSuccess; + } + + get isFailure(): boolean { + return !this._isSuccess; + } + + get data(): T { + return this.getData(); + } + + get error(): E { + return this.getError(); + } + + getError(): E { + if (this._isSuccess) { + throw new Error("Cannot get error from a successful result."); + } + return this._error as E; + } + + getData(): T { + if (!this._isSuccess) { + throw new Error("Cannot get value data from a failed result."); + } + return this._data as T; + } + + map(fn: (value: T) => U): Result { + if (this.isSuccess && this._data !== undefined) { + return Result.ok(fn(this.data)); + } + return Result.fail(this.error || new Error("Unknown error")); + } + + /** + * 🔹 `getOrElse(defaultValue: T): T` + * Si el `Result` es un `ok`, devuelve `data`, de lo contrario, devuelve `defaultValue`. + */ + getOrElse(defaultValue: any): T | any { + return this._isSuccess ? this.data : defaultValue; + } + + /** + * 🔹 `match(onOk: (data: T) => R, onError: (error: E) => R): R` + * Evalúa el `Result`: ejecuta `onOk()` si es `ok` o `onError()` si es `fail`. + */ + match(onOk: (data: T) => R, onError: (error: E) => R): R { + return this._isSuccess ? onOk(this.data) : onError(this.error); + } +} diff --git a/packages/rdx-utils/src/helpers/utils.ts b/packages/rdx-utils/src/helpers/utils.ts new file mode 100644 index 0000000..3f3d075 --- /dev/null +++ b/packages/rdx-utils/src/helpers/utils.ts @@ -0,0 +1,12 @@ +// Función genérica para asegurar valores básicos +function ensure(value: T | undefined | null, defaultValue: T): T { + return value ?? defaultValue; +} + +// Implementaciones específicas para tipos básicos +export const ensureString = (value?: string): string => ensure(value, ""); +export const ensureNumber = (value?: number): number => ensure(value, 0); +export const ensureBoolean = (value?: boolean): boolean => ensure(value, false); +export const ensureBigInt = (value?: bigint): bigint => ensure(value, BigInt(0)); +export const ensureSymbol = (value?: symbol, defaultSymbol = Symbol()): symbol => + ensure(value, defaultSymbol); diff --git a/packages/rdx-utils/src/index.ts b/packages/rdx-utils/src/index.ts new file mode 100644 index 0000000..d4e09d7 --- /dev/null +++ b/packages/rdx-utils/src/index.ts @@ -0,0 +1 @@ +export * from "./helpers"; diff --git a/packages/rdx-utils/tsconfig.json b/packages/rdx-utils/tsconfig.json new file mode 100644 index 0000000..ae936c8 --- /dev/null +++ b/packages/rdx-utils/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["node"], + + "paths": { + "@/*": ["./src/*"] + } + }, + "files": ["src/index.ts"], + "include": ["src/index.ts"], + "exclude": ["node_modules", "dist", "**/*/__tests__"] +} diff --git a/packages/rdx-utils/turbo.json b/packages/rdx-utils/turbo.json new file mode 100644 index 0000000..35be9c6 --- /dev/null +++ b/packages/rdx-utils/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} diff --git a/packages/typescript-config/README.md b/packages/typescript-config/README.md new file mode 100644 index 0000000..0da79cf --- /dev/null +++ b/packages/typescript-config/README.md @@ -0,0 +1,3 @@ +# `tsconfig` + +These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from. diff --git a/packages/typescript-config/base.json b/packages/typescript-config/base.json new file mode 100644 index 0000000..b662acd --- /dev/null +++ b/packages/typescript-config/base.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + "isolatedModules": true, + "moduleResolution": "node", + "noUnusedLocals": false, + "noUnusedParameters": false, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true + }, + "exclude": ["node_modules"] +} diff --git a/packages/typescript-config/library.json b/packages/typescript-config/library.json new file mode 100644 index 0000000..7436690 --- /dev/null +++ b/packages/typescript-config/library.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./base.json", + "compilerOptions": { + "lib": ["ES2022"], + "target": "ES2022" + }, + "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json new file mode 100644 index 0000000..5b8b740 --- /dev/null +++ b/packages/typescript-config/package.json @@ -0,0 +1,12 @@ +{ + "name": "@repo/typescript-config", + "version": "0.0.0", + "private": true, + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "scripts": { + "clean": "rm -rf dist && rm -rf node_modules" + } +} diff --git a/packages/typescript-config/react-library.json b/packages/typescript-config/react-library.json new file mode 100644 index 0000000..c3a1b26 --- /dev/null +++ b/packages/typescript-config/react-library.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./base.json", + "compilerOptions": { + "jsx": "react-jsx" + } +} diff --git a/packages/typescript-config/vite.json b/packages/typescript-config/vite.json new file mode 100644 index 0000000..3c507ed --- /dev/null +++ b/packages/typescript-config/vite.json @@ -0,0 +1,16 @@ +{ + "extends": "./base.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "sourceMap": true, + "resolveJsonModule": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true + }, + "exclude": ["node_modules"] +} diff --git a/packages/ui/.eslintrc.js b/packages/ui/.eslintrc.js new file mode 100644 index 0000000..4cb7125 --- /dev/null +++ b/packages/ui/.eslintrc.js @@ -0,0 +1,9 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["@repo/eslint-config/react-internal.js"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: true, + }, +}; diff --git a/packages/ui/components/counter.tsx b/packages/ui/components/counter.tsx new file mode 100644 index 0000000..3904438 --- /dev/null +++ b/packages/ui/components/counter.tsx @@ -0,0 +1,11 @@ +import React, { useState } from "react"; + +export const Counter: React.FC = () => { + const [count, setCount] = useState(0); + + return ( + + ); +}; diff --git a/packages/ui/components/header.tsx b/packages/ui/components/header.tsx new file mode 100644 index 0000000..b2a6598 --- /dev/null +++ b/packages/ui/components/header.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +interface HeaderProps { + title: string; +} + +export const Header: React.FC = ({ title }) => { + return ( + + ); +}; diff --git a/packages/ui/components/index.ts b/packages/ui/components/index.ts new file mode 100644 index 0000000..611e3f0 --- /dev/null +++ b/packages/ui/components/index.ts @@ -0,0 +1,2 @@ +export * from "./header"; +export * from "./counter"; diff --git a/packages/ui/index.ts b/packages/ui/index.ts new file mode 100644 index 0000000..af24e8b --- /dev/null +++ b/packages/ui/index.ts @@ -0,0 +1,2 @@ +// components +export * from "./components"; diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..2a9a477 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,27 @@ +{ + "name": "@repo/ui", + "version": "0.0.0", + "exports": { + ".": "./index.ts", + "./counter": "./components/counter.ts", + "./header": "./components/header.ts", + "./setup-counter": "./utils/counter.ts" + }, + "license": "MIT", + "scripts": { + "clean": "rm -rf node_modules", + "lint": "eslint \"**/*.ts\"" + }, + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "eslint": "^9.25.1", + "typescript": "5.8.3" + } +} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 0000000..2ce5e4d --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@repo/typescript-config/react-library.json", + "include": ["."], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..5d0f9a6 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,8484 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + eslint: + specifier: ^9.25.1 + version: 9.25.1 + prettier: + specifier: ^3.5.3 + version: 3.5.3 + turbo: + specifier: ^2.5.2 + version: 2.5.2 + + apps/server: + dependencies: + '@modules/invoices': + specifier: workspace:* + version: link:../../modules/invoices + '@rdx/core': + specifier: workspace:* + version: link:../../packages/rdx-core + '@rdx/ddd-domain': + specifier: workspace:* + version: link:../../packages/rdx-ddd-domain + '@rdx/logger': + specifier: workspace:* + version: link:../../packages/rdx-logger + '@rdx/modules': + specifier: workspace:* + version: link:../../packages/rdx-modules + '@rdx/utils': + specifier: workspace:* + version: link:../../packages/rdx-utils + bcrypt: + specifier: ^5.1.1 + version: 5.1.1 + body-parser: + specifier: ^2.2.0 + version: 2.2.0 + cors: + specifier: ^2.8.5 + version: 2.8.5 + dinero.js: + specifier: ^1.9.1 + version: 1.9.1 + dotenv: + specifier: ^16.5.0 + version: 16.5.0 + express: + specifier: ^4.21.2 + version: 4.21.2 + helmet: + specifier: ^8.1.0 + version: 8.1.0 + http-status: + specifier: ^2.1.0 + version: 2.1.0 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + libphonenumber-js: + specifier: ^1.11.20 + version: 1.12.7 + luxon: + specifier: ^3.5.0 + version: 3.6.1 + module-alias: + specifier: ^2.2.3 + version: 2.2.3 + mysql2: + specifier: ^3.12.0 + version: 3.14.0 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + passport-local: + specifier: ^1.0.0 + version: 1.0.0 + path: + specifier: ^0.12.7 + version: 0.12.7 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + response-time: + specifier: ^2.3.3 + version: 2.3.3 + sequelize: + specifier: ^6.37.7 + version: 6.37.7(mysql2@3.14.0) + zod: + specifier: ^3.24.3 + version: 3.24.3 + devDependencies: + '@jest/globals': + specifier: ^29.7.0 + version: 29.7.0 + '@repo/eslint-config': + specifier: workspace:* + version: link:../../packages/eslint-config + '@repo/jest-presets': + specifier: workspace:* + version: link:../../packages/jest-presets + '@repo/typescript-config': + specifier: workspace:* + version: link:../../packages/typescript-config + '@types/bcrypt': + specifier: ^5.0.2 + version: 5.0.2 + '@types/body-parser': + specifier: ^1.19.5 + version: 1.19.5 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.17 + '@types/dinero.js': + specifier: ^1.9.4 + version: 1.9.4 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/glob': + specifier: ^8.1.0 + version: 8.1.0 + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/jsonwebtoken': + specifier: ^9.0.9 + version: 9.0.9 + '@types/luxon': + specifier: ^3.6.2 + version: 3.6.2 + '@types/morgan': + specifier: ^1.9.9 + version: 1.9.9 + '@types/node': + specifier: ^22.15.2 + version: 22.15.2 + '@types/passport': + specifier: ^1.0.17 + version: 1.0.17 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 + '@types/passport-local': + specifier: ^1.0.38 + version: 1.0.38 + '@types/response-time': + specifier: ^2.3.8 + version: 2.3.8 + '@types/supertest': + specifier: ^6.0.3 + version: 6.0.3 + '@typescript-eslint/eslint-plugin': + specifier: ^8.31.0 + version: 8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.31.0 + version: 8.31.0(eslint@9.25.1)(typescript@5.8.3) + esbuild: + specifier: ^0.25.3 + version: 0.25.3 + esbuild-register: + specifier: ^3.6.0 + version: 3.6.0(esbuild@0.25.3) + eslint: + specifier: ^9.25.1 + version: 9.25.1 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.15.2) + nodemon: + specifier: ^3.1.10 + version: 3.1.10 + supertest: + specifier: ^7.1.0 + version: 7.1.0 + typescript: + specifier: 5.8.3 + version: 5.8.3 + + apps/web: + dependencies: + '@repo/ui': + specifier: workspace:* + version: link:../../packages/ui + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + devDependencies: + '@repo/eslint-config': + specifier: workspace:* + version: link:../../packages/eslint-config + '@repo/typescript-config': + specifier: workspace:* + version: link:../../packages/typescript-config + '@types/react': + specifier: ^19.1.2 + version: 19.1.2 + '@types/react-dom': + specifier: ^19.1.2 + version: 19.1.2(@types/react@19.1.2) + '@vitejs/plugin-react': + specifier: ^4.4.1 + version: 4.4.1(vite@6.3.3(@types/node@22.15.2)) + eslint: + specifier: ^9.25.1 + version: 9.25.1 + typescript: + specifier: 5.8.3 + version: 5.8.3 + vite: + specifier: ^6.3.3 + version: 6.3.3(@types/node@22.15.2) + + modules/invoices: + dependencies: + '@rdx/core': + specifier: workspace:* + version: link:../../packages/rdx-core + '@rdx/ddd-domain': + specifier: workspace:* + version: link:../../packages/rdx-ddd-domain + '@rdx/logger': + specifier: workspace:* + version: link:../../packages/rdx-logger + '@rdx/modules': + specifier: workspace:* + version: link:../../packages/rdx-modules + '@rdx/utils': + specifier: workspace:* + version: link:../../packages/rdx-utils + bcrypt: + specifier: ^5.1.1 + version: 5.1.1 + body-parser: + specifier: ^2.2.0 + version: 2.2.0 + cors: + specifier: ^2.8.5 + version: 2.8.5 + dinero.js: + specifier: ^1.9.1 + version: 1.9.1 + dotenv: + specifier: ^16.5.0 + version: 16.5.0 + express: + specifier: ^4.21.2 + version: 4.21.2 + helmet: + specifier: ^8.1.0 + version: 8.1.0 + http-status: + specifier: ^2.1.0 + version: 2.1.0 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + libphonenumber-js: + specifier: ^1.11.20 + version: 1.12.7 + luxon: + specifier: ^3.5.0 + version: 3.6.1 + module-alias: + specifier: ^2.2.3 + version: 2.2.3 + mysql2: + specifier: ^3.12.0 + version: 3.14.0 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + passport-local: + specifier: ^1.0.0 + version: 1.0.0 + path: + specifier: ^0.12.7 + version: 0.12.7 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + response-time: + specifier: ^2.3.3 + version: 2.3.3 + sequelize: + specifier: ^6.37.7 + version: 6.37.7(mysql2@3.14.0) + zod: + specifier: ^3.24.3 + version: 3.24.3 + devDependencies: + '@jest/globals': + specifier: ^29.7.0 + version: 29.7.0 + '@repo/eslint-config': + specifier: workspace:* + version: link:../../packages/eslint-config + '@repo/jest-presets': + specifier: workspace:* + version: link:../../packages/jest-presets + '@repo/typescript-config': + specifier: workspace:* + version: link:../../packages/typescript-config + '@types/bcrypt': + specifier: ^5.0.2 + version: 5.0.2 + '@types/body-parser': + specifier: ^1.19.5 + version: 1.19.5 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.17 + '@types/dinero.js': + specifier: ^1.9.4 + version: 1.9.4 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/glob': + specifier: ^8.1.0 + version: 8.1.0 + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/jsonwebtoken': + specifier: ^9.0.9 + version: 9.0.9 + '@types/luxon': + specifier: ^3.6.2 + version: 3.6.2 + '@types/morgan': + specifier: ^1.9.9 + version: 1.9.9 + '@types/node': + specifier: ^22.15.2 + version: 22.15.2 + '@types/passport': + specifier: ^1.0.17 + version: 1.0.17 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 + '@types/passport-local': + specifier: ^1.0.38 + version: 1.0.38 + '@types/response-time': + specifier: ^2.3.8 + version: 2.3.8 + '@types/supertest': + specifier: ^6.0.3 + version: 6.0.3 + '@typescript-eslint/eslint-plugin': + specifier: ^8.31.0 + version: 8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.31.0 + version: 8.31.0(eslint@9.25.1)(typescript@5.8.3) + esbuild: + specifier: ^0.25.3 + version: 0.25.3 + esbuild-register: + specifier: ^3.6.0 + version: 3.6.0(esbuild@0.25.3) + eslint: + specifier: ^9.25.1 + version: 9.25.1 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.15.2) + nodemon: + specifier: ^3.1.10 + version: 3.1.10 + supertest: + specifier: ^7.1.0 + version: 7.1.0 + tsc-alias: + specifier: ^1.8.15 + version: 1.8.15 + typescript: + specifier: 5.8.3 + version: 5.8.3 + + packages/eslint-config: + devDependencies: + '@typescript-eslint/eslint-plugin': + specifier: ^8.31.0 + version: 8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.31.0 + version: 8.31.0(eslint@9.25.1)(typescript@5.8.3) + eslint-config-prettier: + specifier: ^10.1.2 + version: 10.1.2(eslint@9.25.1) + eslint-config-turbo: + specifier: ^2.5.2 + version: 2.5.2(eslint@9.25.1)(turbo@2.5.2) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1) + eslint-plugin-only-warn: + specifier: ^1.1.0 + version: 1.1.0 + typescript: + specifier: 5.8.3 + version: 5.8.3 + + packages/jest-presets: + dependencies: + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.15.2) + ts-jest: + specifier: ^29.3.2 + version: 29.3.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest@29.7.0)(typescript@5.8.3) + + packages/rdx-auth: + dependencies: + '@rdx/core': + specifier: workspace:* + version: link:../rdx-core + '@rdx/ddd-domain': + specifier: workspace:* + version: link:../rdx-ddd-domain + '@rdx/logger': + specifier: workspace:* + version: link:../rdx-logger + '@rdx/modules': + specifier: workspace:* + version: link:../rdx-modules + '@rdx/utils': + specifier: workspace:* + version: link:../rdx-utils + bcrypt: + specifier: ^5.1.1 + version: 5.1.1 + body-parser: + specifier: ^2.2.0 + version: 2.2.0 + cors: + specifier: ^2.8.5 + version: 2.8.5 + dinero.js: + specifier: ^1.9.1 + version: 1.9.1 + dotenv: + specifier: ^16.5.0 + version: 16.5.0 + express: + specifier: ^4.21.2 + version: 4.21.2 + helmet: + specifier: ^8.1.0 + version: 8.1.0 + http-status: + specifier: ^2.1.0 + version: 2.1.0 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + libphonenumber-js: + specifier: ^1.11.20 + version: 1.12.7 + luxon: + specifier: ^3.5.0 + version: 3.6.1 + module-alias: + specifier: ^2.2.3 + version: 2.2.3 + mysql2: + specifier: ^3.12.0 + version: 3.14.0 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + passport-local: + specifier: ^1.0.0 + version: 1.0.0 + path: + specifier: ^0.12.7 + version: 0.12.7 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + response-time: + specifier: ^2.3.3 + version: 2.3.3 + sequelize: + specifier: ^6.37.7 + version: 6.37.7(mysql2@3.14.0) + zod: + specifier: ^3.24.3 + version: 3.24.3 + devDependencies: + '@jest/globals': + specifier: ^29.7.0 + version: 29.7.0 + '@repo/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@repo/jest-presets': + specifier: workspace:* + version: link:../jest-presets + '@repo/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/bcrypt': + specifier: ^5.0.2 + version: 5.0.2 + '@types/body-parser': + specifier: ^1.19.5 + version: 1.19.5 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.17 + '@types/dinero.js': + specifier: ^1.9.4 + version: 1.9.4 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/glob': + specifier: ^8.1.0 + version: 8.1.0 + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/jsonwebtoken': + specifier: ^9.0.9 + version: 9.0.9 + '@types/luxon': + specifier: ^3.6.2 + version: 3.6.2 + '@types/morgan': + specifier: ^1.9.9 + version: 1.9.9 + '@types/node': + specifier: ^22.15.2 + version: 22.15.2 + '@types/passport': + specifier: ^1.0.17 + version: 1.0.17 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 + '@types/passport-local': + specifier: ^1.0.38 + version: 1.0.38 + '@types/response-time': + specifier: ^2.3.8 + version: 2.3.8 + '@types/supertest': + specifier: ^6.0.3 + version: 6.0.3 + '@typescript-eslint/eslint-plugin': + specifier: ^8.31.0 + version: 8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.31.0 + version: 8.31.0(eslint@9.25.1)(typescript@5.8.3) + esbuild: + specifier: ^0.25.3 + version: 0.25.3 + esbuild-register: + specifier: ^3.6.0 + version: 3.6.0(esbuild@0.25.3) + eslint: + specifier: ^9.25.1 + version: 9.25.1 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.15.2) + nodemon: + specifier: ^3.1.10 + version: 3.1.10 + supertest: + specifier: ^7.1.0 + version: 7.1.0 + typescript: + specifier: 5.8.3 + version: 5.8.3 + + packages/rdx-core: + dependencies: + '@rdx/ddd-domain': + specifier: workspace:* + version: link:../rdx-ddd-domain + '@rdx/logger': + specifier: workspace:* + version: link:../rdx-logger + '@rdx/modules': + specifier: workspace:* + version: link:../rdx-modules + '@rdx/utils': + specifier: workspace:* + version: link:../rdx-utils + bcrypt: + specifier: ^5.1.1 + version: 5.1.1 + body-parser: + specifier: ^2.2.0 + version: 2.2.0 + cors: + specifier: ^2.8.5 + version: 2.8.5 + dinero.js: + specifier: ^1.9.1 + version: 1.9.1 + dotenv: + specifier: ^16.5.0 + version: 16.5.0 + express: + specifier: ^4.21.2 + version: 4.21.2 + helmet: + specifier: ^8.1.0 + version: 8.1.0 + http-status: + specifier: ^2.1.0 + version: 2.1.0 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + libphonenumber-js: + specifier: ^1.11.20 + version: 1.12.7 + luxon: + specifier: ^3.5.0 + version: 3.6.1 + module-alias: + specifier: ^2.2.3 + version: 2.2.3 + mysql2: + specifier: ^3.12.0 + version: 3.14.0 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + passport-local: + specifier: ^1.0.0 + version: 1.0.0 + path: + specifier: ^0.12.7 + version: 0.12.7 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + response-time: + specifier: ^2.3.3 + version: 2.3.3 + sequelize: + specifier: ^6.37.7 + version: 6.37.7(mysql2@3.14.0) + zod: + specifier: ^3.24.3 + version: 3.24.3 + devDependencies: + '@jest/globals': + specifier: ^29.7.0 + version: 29.7.0 + '@repo/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@repo/jest-presets': + specifier: workspace:* + version: link:../jest-presets + '@repo/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/bcrypt': + specifier: ^5.0.2 + version: 5.0.2 + '@types/body-parser': + specifier: ^1.19.5 + version: 1.19.5 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.17 + '@types/dinero.js': + specifier: ^1.9.4 + version: 1.9.4 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/glob': + specifier: ^8.1.0 + version: 8.1.0 + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/jsonwebtoken': + specifier: ^9.0.9 + version: 9.0.9 + '@types/luxon': + specifier: ^3.6.2 + version: 3.6.2 + '@types/morgan': + specifier: ^1.9.9 + version: 1.9.9 + '@types/node': + specifier: ^22.15.2 + version: 22.15.2 + '@types/passport': + specifier: ^1.0.17 + version: 1.0.17 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 + '@types/passport-local': + specifier: ^1.0.38 + version: 1.0.38 + '@types/response-time': + specifier: ^2.3.8 + version: 2.3.8 + '@types/supertest': + specifier: ^6.0.3 + version: 6.0.3 + '@typescript-eslint/eslint-plugin': + specifier: ^8.31.0 + version: 8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.31.0 + version: 8.31.0(eslint@9.25.1)(typescript@5.8.3) + esbuild: + specifier: ^0.25.3 + version: 0.25.3 + esbuild-register: + specifier: ^3.6.0 + version: 3.6.0(esbuild@0.25.3) + eslint: + specifier: ^9.25.1 + version: 9.25.1 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.15.2) + nodemon: + specifier: ^3.1.10 + version: 3.1.10 + supertest: + specifier: ^7.1.0 + version: 7.1.0 + typescript: + specifier: 5.8.3 + version: 5.8.3 + + packages/rdx-criteria: + devDependencies: + '@changesets/cli': + specifier: ^2.29.2 + version: 2.29.2 + '@faker-js/faker': + specifier: ^9.7.0 + version: 9.7.0 + '@repo/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@repo/jest-presets': + specifier: workspace:* + version: link:../jest-presets + '@repo/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/node': + specifier: ^22.15.2 + version: 22.15.2 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.15.2) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + + packages/rdx-ddd-domain: + dependencies: + '@rdx/logger': + specifier: workspace:* + version: link:../rdx-logger + '@rdx/utils': + specifier: workspace:* + version: link:../rdx-utils + dinero.js: + specifier: ^1.9.1 + version: 1.9.1 + libphonenumber-js: + specifier: ^1.12.7 + version: 1.12.7 + shallow-equal-object: + specifier: ^1.1.1 + version: 1.1.1 + uuid: + specifier: ^11.1.0 + version: 11.1.0 + zod: + specifier: ^3.24.3 + version: 3.24.3 + devDependencies: + '@repo/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@repo/jest-presets': + specifier: workspace:* + version: link:../jest-presets + '@repo/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/dinero.js': + specifier: ^1.9.4 + version: 1.9.4 + '@types/node': + specifier: ^22.15.2 + version: 22.15.2 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.15.2) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + + packages/rdx-logger: + dependencies: + cls-rtracer: + specifier: ^2.6.3 + version: 2.6.3 + dotenv: + specifier: ^16.5.0 + version: 16.5.0 + path: + specifier: ^0.12.7 + version: 0.12.7 + winston: + specifier: ^3.17.0 + version: 3.17.0 + winston-daily-rotate-file: + specifier: ^5.0.0 + version: 5.0.0(winston@3.17.0) + devDependencies: + '@jest/globals': + specifier: ^29.7.0 + version: 29.7.0 + '@repo/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@repo/jest-presets': + specifier: workspace:* + version: link:../jest-presets + '@repo/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/dotenv': + specifier: ^8.2.3 + version: 8.2.3 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/node': + specifier: ^22.15.2 + version: 22.15.2 + '@types/react': + specifier: ^19.1.2 + version: 19.1.2 + '@types/winston': + specifier: ^2.4.4 + version: 2.4.4 + eslint: + specifier: ^9.25.1 + version: 9.25.1 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.15.2) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + + packages/rdx-modules: + dependencies: + express: + specifier: ^4.21.2 + version: 4.21.2 + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + sequelize: + specifier: ^6.37.7 + version: 6.37.7(mysql2@3.14.0) + devDependencies: + '@repo/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@repo/jest-presets': + specifier: workspace:* + version: link:../jest-presets + '@repo/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/node': + specifier: ^22.15.2 + version: 22.15.2 + '@types/react': + specifier: ^19.1.2 + version: 19.1.2 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.15.2) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + + packages/rdx-utils: + devDependencies: + '@repo/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@repo/jest-presets': + specifier: workspace:* + version: link:../jest-presets + '@repo/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/node': + specifier: ^22.15.2 + version: 22.15.2 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + + packages/typescript-config: {} + + packages/ui: + dependencies: + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + devDependencies: + '@repo/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@repo/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/react': + specifier: ^19.1.2 + version: 19.1.2 + '@types/react-dom': + specifier: ^19.1.2 + version: 19.1.2(@types/react@19.1.2) + eslint: + specifier: ^9.25.1 + version: 9.25.1 + typescript: + specifier: 5.8.3 + version: 5.8.3 + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.8': + resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.10': + resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.27.0': + resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.0': + resolution: {integrity: sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.26.5': + resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.0': + resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.0': + resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.26.0': + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.25.9': + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.25.9': + resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.25.9': + resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.27.0': + resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.0': + resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.27.0': + resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.0': + resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@changesets/apply-release-plan@7.0.12': + resolution: {integrity: sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==} + + '@changesets/assemble-release-plan@6.0.6': + resolution: {integrity: sha512-Frkj8hWJ1FRZiY3kzVCKzS0N5mMwWKwmv9vpam7vt8rZjLL1JMthdh6pSDVSPumHPshTTkKZ0VtNbE0cJHZZUg==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.29.2': + resolution: {integrity: sha512-vwDemKjGYMOc0l6WUUTGqyAWH3AmueeyoJa1KmFRtCYiCoY5K3B68ErYpDB6H48T4lLI4czum4IEjh6ildxUeg==} + hasBin: true + + '@changesets/config@3.1.1': + resolution: {integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.3': + resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} + + '@changesets/get-release-plan@4.0.10': + resolution: {integrity: sha512-CCJ/f3edYaA3MqoEnWvGGuZm0uMEMzNJ97z9hdUR34AOvajSwySwsIzC/bBu3+kuGDsB+cny4FljG8UBWAa7jg==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.1': + resolution: {integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.5': + resolution: {integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + + '@esbuild/aix-ppc64@0.25.3': + resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.3': + resolution: {integrity: sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.3': + resolution: {integrity: sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.3': + resolution: {integrity: sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.3': + resolution: {integrity: sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.3': + resolution: {integrity: sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.3': + resolution: {integrity: sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.3': + resolution: {integrity: sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.3': + resolution: {integrity: sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.3': + resolution: {integrity: sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.3': + resolution: {integrity: sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.3': + resolution: {integrity: sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.3': + resolution: {integrity: sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.3': + resolution: {integrity: sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.3': + resolution: {integrity: sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.3': + resolution: {integrity: sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.3': + resolution: {integrity: sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.3': + resolution: {integrity: sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.3': + resolution: {integrity: sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.3': + resolution: {integrity: sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.3': + resolution: {integrity: sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.3': + resolution: {integrity: sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.3': + resolution: {integrity: sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.3': + resolution: {integrity: sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.3': + resolution: {integrity: sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.6.1': + resolution: {integrity: sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.20.0': + resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.2.1': + resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.13.0': + resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.25.1': + resolution: {integrity: sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.8': + resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@faker-js/faker@9.7.0': + resolution: {integrity: sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg==} + engines: {node: '>=18.0.0', npm: '>=9.0.0'} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.2': + resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} + engines: {node: '>=18.18'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@mapbox/node-pre-gyp@1.0.11': + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@paralleldrive/cuid2@2.2.2': + resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} + + '@rollup/rollup-android-arm-eabi@4.40.0': + resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.40.0': + resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.40.0': + resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.40.0': + resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.40.0': + resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.40.0': + resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.40.0': + resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.40.0': + resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.40.0': + resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.40.0': + resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.40.0': + resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': + resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.40.0': + resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.40.0': + resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.40.0': + resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.40.0': + resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.40.0': + resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.40.0': + resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.40.0': + resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.40.0': + resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + + '@types/bcrypt@5.0.2': + resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==} + + '@types/body-parser@1.19.5': + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + + '@types/cors@2.8.17': + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/dinero.js@1.9.4': + resolution: {integrity: sha512-mtJnan4ajy9MqvoJGVXu0tC9EAAzFjeoKc3d+8AW+H/Od9+8IiC59ymjrZF+JdTToyDvkLReacTsc50Z8eYr6Q==} + + '@types/dotenv@8.2.3': + resolution: {integrity: sha512-g2FXjlDX/cYuc5CiQvyU/6kkbP1JtmGzh0obW50zD7OKeILVL0NSpPWLXVfqoAGQjom2/SLLx9zHq0KXvD6mbw==} + deprecated: This is a stub types definition. dotenv provides its own type definitions, so you do not need this installed. + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/express-serve-static-core@4.19.6': + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + + '@types/express@4.17.21': + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + + '@types/glob@8.1.0': + resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/http-errors@2.0.4': + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/jsonwebtoken@9.0.9': + resolution: {integrity: sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==} + + '@types/luxon@3.6.2': + resolution: {integrity: sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==} + + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/minimatch@5.1.2': + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + + '@types/morgan@1.9.9': + resolution: {integrity: sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@22.15.2': + resolution: {integrity: sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==} + + '@types/passport-jwt@4.0.1': + resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} + + '@types/passport-local@1.0.38': + resolution: {integrity: sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==} + + '@types/passport-strategy@0.2.38': + resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==} + + '@types/passport@1.0.17': + resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + + '@types/qs@6.9.18': + resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@19.1.2': + resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.2': + resolution: {integrity: sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==} + + '@types/response-time@2.3.8': + resolution: {integrity: sha512-7qGaNYvdxc0zRab8oHpYx7AW17qj+G0xuag1eCrw3M2VWPJQ/HyKaaghWygiaOUl0y9x7QGQwppDpqLJ5V9pzw==} + + '@types/send@0.17.4': + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + + '@types/serve-static@1.15.7': + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/superagent@8.1.9': + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + + '@types/supertest@6.0.3': + resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/validator@13.15.0': + resolution: {integrity: sha512-nh7nrWhLr6CBq9ldtw0wx+z9wKnnv/uTVLA9g/3/TcOYxbpOSZE+MhKPmWqU+K0NvThjhv12uD8MuqijB0WzEA==} + + '@types/winston@2.4.4': + resolution: {integrity: sha512-BVGCztsypW8EYwJ+Hq+QNYiT/MUyCif0ouBH+flrY66O5W+KIXAMML6E/0fJpm7VjIzgangahl5S03bJJQGrZw==} + deprecated: This is a stub types definition. winston provides its own type definitions, so you do not need this installed. + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + + '@typescript-eslint/eslint-plugin@8.31.0': + resolution: {integrity: sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/parser@8.31.0': + resolution: {integrity: sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/scope-manager@8.31.0': + resolution: {integrity: sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.31.0': + resolution: {integrity: sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/types@8.31.0': + resolution: {integrity: sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.31.0': + resolution: {integrity: sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/utils@8.31.0': + resolution: {integrity: sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/visitor-keys@8.31.0': + resolution: {integrity: sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@4.4.1': + resolution: {integrity: sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-preset-current-node-syntax@1.1.0: + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bcrypt@5.1.1: + resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} + engines: {node: '>= 10.0.0'} + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001715: + resolution: {integrity: sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + cls-rtracer@2.6.3: + resolution: {integrity: sha512-O7M/m2M/KfT9v+q7ka9nmsadS67ce9P8+1Zgm6VFamK56oFd1iCoJ9m8hYKUQpK4+RofyaexxHJlOBkxqCDs3Q==} + engines: {node: '>=12.17.0 <13.0.0 || >=13.14.0 <14.0.0 || >=14.0.0'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + dinero.js@1.9.1: + resolution: {integrity: sha512-1HXiF2vv3ZeRQ23yr+9lFxj/PbZqutuYWJnE0qfCB9xYBPnuaJ8lXtli1cJM0TvUXW1JTOaePldmqN5JVNxKSA==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dotenv@16.0.3: + resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} + engines: {node: '>=12'} + + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + engines: {node: '>=12'} + + dottie@2.0.6: + resolution: {integrity: sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-to-chromium@1.5.142: + resolution: {integrity: sha512-Ah2HgkTu/9RhTDNThBtzu2Wirdy4DC9b0sMT1pUhbkZQ5U/iwmE+PHZX1MpjD5IkJCc2wSghgGG/B04szAx07w==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-abstract@1.23.9: + resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.25.3: + resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.2: + resolution: {integrity: sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-config-turbo@2.5.2: + resolution: {integrity: sha512-aZdMUCJE5sC9UHZPOu6GiqCijw0HzlcxTbGZeESMFanYShhxrO6mAZy02zYOYK+y184HnxAngcfmtgDt8cIUxw==} + peerDependencies: + eslint: '>6.6.0' + turbo: '>2.0.0' + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.12.0: + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.31.0: + resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-only-warn@1.1.0: + resolution: {integrity: sha512-2tktqUAT+Q3hCAU0iSf4xAN1k9zOpjK5WO8104mB0rT/dGhOa09582HN5HlbxNbPRZ0THV7nLGvzugcNOSjzfA==} + engines: {node: '>=6'} + + eslint-plugin-turbo@2.5.2: + resolution: {integrity: sha512-B+vdgOtBuDbRI3sIRayV3HZK1XcwlNuksmJJIgggkXTsNehMEbreJBkIda4qvA/STHnGAl2bUGev0Jx8Rijiwg==} + peerDependencies: + eslint: '>6.6.0' + turbo: '>2.0.0' + + eslint-scope@8.3.0: + resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.25.1: + resolution: {integrity: sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fdir@6.4.4: + resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-stream-rotator@0.6.1: + resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.0: + resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + helmet@8.1.0: + resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} + engines: {node: '>=18.0.0'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-status@2.1.0: + resolution: {integrity: sha512-O5kPr7AW7wYd/BBiOezTwnVAnmSNFY+J7hlZD2X5IOxVBetjcHAiTXhzj0gMrnojQlwy+UT1/Y3H3vJ3UlmvLA==} + engines: {node: '>= 0.4.0'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + human-id@4.1.1: + resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==} + hasBin: true + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflection@1.13.4: + resolution: {integrity: sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==} + engines: {'0': node >= 0.4.0} + + inflight@1.0.6: + resolution: {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. + + inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + libphonenumber-js@1.12.7: + resolution: {integrity: sha512-0nYZSNj/QEikyhcM5RZFXGlCB/mr4PVamnT1C2sKBnDDTYndrvbybYjvg+PMqAndQHlLbwQ3socolnL3WWTUFA==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lru.min@1.1.2: + resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + + luxon@3.6.1: + resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} + engines: {node: '>=12'} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + module-alias@2.2.3: + resolution: {integrity: sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==} + + moment-timezone@0.5.48: + resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mylas@2.1.13: + resolution: {integrity: sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==} + engines: {node: '>=12.0.0'} + + mysql2@3.14.0: + resolution: {integrity: sha512-8eMhmG6gt/hRkU1G+8KlGOdQi2w+CgtNoD1ksXZq9gQfkfDsX4LHaBwTe1SY0Imx//t2iZA03DFnyYKPinxSRw==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + node-addon-api@5.1.0: + resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + nodemon@3.1.10: + resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==} + engines: {node: '>=10'} + hasBin: true + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + + passport-local@1.0.0: + resolution: {integrity: sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==} + engines: {node: '>= 0.4.0'} + + passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + + passport@0.7.0: + resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==} + engines: {node: '>= 0.4.0'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + path@0.12.7: + resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} + + pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + + pg-connection-string@2.8.5: + resolution: {integrity: sha512-Ni8FuZ8yAF+sWZzojvtLE2b03cqjO5jNULcHFfM9ZZ0/JXrgom5pBREbtnAw7oxsxJqHw9Nz/XWORUEL3/IFow==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@3.5.3: + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + + queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + response-time@2.3.3: + resolution: {integrity: sha512-SsjjOPHl/FfrTQNgmc5oen8Hr1Jxpn6LlHNXxCIFdYMHuK1kMeYMobb9XN3mvxaGQm3dbegqYFMX4+GDORfbWg==} + engines: {node: '>= 0.8.0'} + + retry-as-promised@7.1.1: + resolution: {integrity: sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.40.0: + resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + + sequelize-pool@7.1.0: + resolution: {integrity: sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==} + engines: {node: '>= 10.0.0'} + + sequelize@6.37.7: + resolution: {integrity: sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==} + engines: {node: '>=10.0.0'} + peerDependencies: + ibm_db: '*' + mariadb: '*' + mysql2: '*' + oracledb: '*' + pg: '*' + pg-hstore: '*' + snowflake-sdk: '*' + sqlite3: '*' + tedious: '*' + peerDependenciesMeta: + ibm_db: + optional: true + mariadb: + optional: true + mysql2: + optional: true + oracledb: + optional: true + pg: + optional: true + pg-hstore: + optional: true + snowflake-sdk: + optional: true + sqlite3: + optional: true + tedious: + optional: true + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shallow-equal-object@1.1.1: + resolution: {integrity: sha512-9DDzYRlzCwF2CemeF0aOFk5T5KMrjG7HldcW7utwYhA/limuGHn3No8KhpDE8BrO7GLaSRJumNKReipZBybd7A==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + superagent@9.0.2: + resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==} + engines: {node: '>=14.18.0'} + + supertest@7.1.0: + resolution: {integrity: sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==} + engines: {node: '>=14.18.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + tinyglobby@0.2.13: + resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} + engines: {node: '>=12.0.0'} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + toposort-class@1.0.1: + resolution: {integrity: sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==} + + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-jest@29.3.2: + resolution: {integrity: sha512-bJJkrWc6PjFVz5g2DGCNUo8z7oFEYaz1xP1NpeDU7KNLMWPpEyV8Chbpkn8xjzgRDpQhnGMyvyldoL7h8JXyug==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + + tsc-alias@1.8.15: + resolution: {integrity: sha512-yKLVx8ddUurRwhVcS6JFF2ZjksOX2ZWDRIdgt+PQhJBDegIdAdilptiHsuAbx9UFxa16GFrxeKQ2kTcGvR6fkQ==} + engines: {node: '>=16.20.2'} + hasBin: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + turbo-darwin-64@2.5.2: + resolution: {integrity: sha512-2aIl0Sx230nLk+Cg2qSVxvPOBWCZpwKNuAMKoROTvWKif6VMpkWWiR9XEPoz7sHeLmCOed4GYGMjL1bqAiIS/g==} + cpu: [x64] + os: [darwin] + + turbo-darwin-arm64@2.5.2: + resolution: {integrity: sha512-MrFYhK/jYu8N6QlqZtqSHi3e4QVxlzqU3ANHTKn3/tThuwTLbNHEvzBPWSj5W7nZcM58dCqi6gYrfRz6bJZyAA==} + cpu: [arm64] + os: [darwin] + + turbo-linux-64@2.5.2: + resolution: {integrity: sha512-LxNqUE2HmAJQ/8deoLgMUDzKxd5bKxqH0UBogWa+DF+JcXhtze3UTMr6lEr0dEofdsEUYK1zg8FRjglmwlN5YA==} + cpu: [x64] + os: [linux] + + turbo-linux-arm64@2.5.2: + resolution: {integrity: sha512-0MI1Ao1q8zhd+UUbIEsrM+yLq1BsrcJQRGZkxIsHFlGp7WQQH1oR3laBgfnUCNdCotCMD6w4moc9pUbXdOR3bg==} + cpu: [arm64] + os: [linux] + + turbo-windows-64@2.5.2: + resolution: {integrity: sha512-hOLcbgZzE5ttACHHyc1ajmWYq4zKT42IC3G6XqgiXxMbS+4eyVYTL+7UvCZBd3Kca1u4TLQdLQjeO76zyDJc2A==} + cpu: [x64] + os: [win32] + + turbo-windows-arm64@2.5.2: + resolution: {integrity: sha512-fMU41ABhSLa18H8V3Z7BMCGynQ8x+wj9WyBMvWm1jeyRKgkvUYJsO2vkIsy8m0vrwnIeVXKOIn6eSe1ddlBVqw==} + cpu: [arm64] + os: [win32] + + turbo@2.5.2: + resolution: {integrity: sha512-Qo5lfuStr6LQh3sPQl7kIi243bGU4aHGDQJUf6ylAdGwks30jJFloc9NYHP7Y373+gGU9OS0faA4Mb5Sy8X9Xw==} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.40.0: + resolution: {integrity: sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==} + engines: {node: '>=16'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.10.4: + resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + validator@13.15.0: + resolution: {integrity: sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==} + engines: {node: '>= 0.10'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite@6.3.3: + resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + winston-daily-rotate-file@5.0.0: + resolution: {integrity: sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==} + engines: {node: '>=8'} + peerDependencies: + winston: ^3 + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.17.0: + resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} + engines: {node: '>= 12.0.0'} + + wkx@0.5.0: + resolution: {integrity: sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod@3.24.3: + resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.8': {} + + '@babel/core@7.26.10': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.27.0 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helpers': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/template': 7.27.0 + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + convert-source-map: 2.0.0 + debug: 4.4.0(supports-color@5.5.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.27.0': + dependencies: + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.0': + dependencies: + '@babel/compat-data': 7.26.8 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.26.5': {} + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helpers@7.27.0': + dependencies: + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 + + '@babel/parser@7.27.0': + dependencies: + '@babel/types': 7.27.0 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/runtime@7.27.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.27.0': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + + '@babel/traverse@7.27.0': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 + debug: 4.4.0(supports-color@5.5.0) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.27.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@bcoe/v8-coverage@0.2.3': {} + + '@changesets/apply-release-plan@7.0.12': + dependencies: + '@changesets/config': 3.1.1 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.7.1 + + '@changesets/assemble-release-plan@6.0.6': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.7.1 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.29.2': + dependencies: + '@changesets/apply-release-plan': 7.0.12 + '@changesets/assemble-release-plan': 6.0.6 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.1 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-release-plan': 4.0.10 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.5 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + ci-info: 3.9.0 + enquirer: 2.4.1 + external-editor: 3.1.0 + fs-extra: 7.0.1 + mri: 1.2.0 + p-limit: 2.3.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.7.1 + spawndamnit: 3.0.1 + term-size: 2.2.1 + + '@changesets/config@3.1.1': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/logger': 0.1.1 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.3': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.7.1 + + '@changesets/get-release-plan@4.0.10': + dependencies: + '@changesets/assemble-release-plan': 6.0.6 + '@changesets/config': 3.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.5 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.1': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 3.14.1 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.5': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.1 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.1.1 + prettier: 2.8.8 + + '@colors/colors@1.6.0': {} + + '@dabh/diagnostics@2.0.3': + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + + '@esbuild/aix-ppc64@0.25.3': + optional: true + + '@esbuild/android-arm64@0.25.3': + optional: true + + '@esbuild/android-arm@0.25.3': + optional: true + + '@esbuild/android-x64@0.25.3': + optional: true + + '@esbuild/darwin-arm64@0.25.3': + optional: true + + '@esbuild/darwin-x64@0.25.3': + optional: true + + '@esbuild/freebsd-arm64@0.25.3': + optional: true + + '@esbuild/freebsd-x64@0.25.3': + optional: true + + '@esbuild/linux-arm64@0.25.3': + optional: true + + '@esbuild/linux-arm@0.25.3': + optional: true + + '@esbuild/linux-ia32@0.25.3': + optional: true + + '@esbuild/linux-loong64@0.25.3': + optional: true + + '@esbuild/linux-mips64el@0.25.3': + optional: true + + '@esbuild/linux-ppc64@0.25.3': + optional: true + + '@esbuild/linux-riscv64@0.25.3': + optional: true + + '@esbuild/linux-s390x@0.25.3': + optional: true + + '@esbuild/linux-x64@0.25.3': + optional: true + + '@esbuild/netbsd-arm64@0.25.3': + optional: true + + '@esbuild/netbsd-x64@0.25.3': + optional: true + + '@esbuild/openbsd-arm64@0.25.3': + optional: true + + '@esbuild/openbsd-x64@0.25.3': + optional: true + + '@esbuild/sunos-x64@0.25.3': + optional: true + + '@esbuild/win32-arm64@0.25.3': + optional: true + + '@esbuild/win32-ia32@0.25.3': + optional: true + + '@esbuild/win32-x64@0.25.3': + optional: true + + '@eslint-community/eslint-utils@4.6.1(eslint@9.25.1)': + dependencies: + eslint: 9.25.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.20.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.0(supports-color@5.5.0) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.2.1': {} + + '@eslint/core@0.13.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.0(supports-color@5.5.0) + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.25.1': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.2.8': + dependencies: + '@eslint/core': 0.13.0 + levn: 0.4.1 + + '@faker-js/faker@9.7.0': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.2': {} + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.15.2 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.2 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.15.2) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.2 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 22.15.2 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + '@types/node': 22.15.2 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.26.10 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.15.2 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.27.0 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.27.0 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@mapbox/node-pre-gyp@1.0.11': + dependencies: + detect-libc: 2.0.4 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.7.1 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@noble/hashes@1.8.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@paralleldrive/cuid2@2.2.2': + dependencies: + '@noble/hashes': 1.8.0 + + '@rollup/rollup-android-arm-eabi@4.40.0': + optional: true + + '@rollup/rollup-android-arm64@4.40.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.40.0': + optional: true + + '@rollup/rollup-darwin-x64@4.40.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.40.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.40.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.40.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.40.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.40.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.40.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.40.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.40.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.40.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.40.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.40.0': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@sinclair/typebox@0.27.8': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.27.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.27.0 + + '@types/bcrypt@5.0.2': + dependencies: + '@types/node': 22.15.2 + + '@types/body-parser@1.19.5': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.15.2 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.15.2 + + '@types/cookiejar@2.1.5': {} + + '@types/cors@2.8.17': + dependencies: + '@types/node': 22.15.2 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/dinero.js@1.9.4': {} + + '@types/dotenv@8.2.3': + dependencies: + dotenv: 16.5.0 + + '@types/estree@1.0.7': {} + + '@types/express-serve-static-core@4.19.6': + dependencies: + '@types/node': 22.15.2 + '@types/qs': 6.9.18 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express@4.17.21': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.19.6 + '@types/qs': 6.9.18 + '@types/serve-static': 1.15.7 + + '@types/glob@8.1.0': + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 22.15.2 + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.15.2 + + '@types/http-errors@2.0.4': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/jsonwebtoken@9.0.9': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 22.15.2 + + '@types/luxon@3.6.2': {} + + '@types/methods@1.1.4': {} + + '@types/mime@1.3.5': {} + + '@types/minimatch@5.1.2': {} + + '@types/morgan@1.9.9': + dependencies: + '@types/node': 22.15.2 + + '@types/ms@2.1.0': {} + + '@types/node@12.20.55': {} + + '@types/node@22.15.2': + dependencies: + undici-types: 6.21.0 + + '@types/passport-jwt@4.0.1': + dependencies: + '@types/jsonwebtoken': 9.0.9 + '@types/passport-strategy': 0.2.38 + + '@types/passport-local@1.0.38': + dependencies: + '@types/express': 4.17.21 + '@types/passport': 1.0.17 + '@types/passport-strategy': 0.2.38 + + '@types/passport-strategy@0.2.38': + dependencies: + '@types/express': 4.17.21 + '@types/passport': 1.0.17 + + '@types/passport@1.0.17': + dependencies: + '@types/express': 4.17.21 + + '@types/qs@6.9.18': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@19.1.2(@types/react@19.1.2)': + dependencies: + '@types/react': 19.1.2 + + '@types/react@19.1.2': + dependencies: + csstype: 3.1.3 + + '@types/response-time@2.3.8': + dependencies: + '@types/express': 4.17.21 + '@types/node': 22.15.2 + + '@types/send@0.17.4': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.15.2 + + '@types/serve-static@1.15.7': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 22.15.2 + '@types/send': 0.17.4 + + '@types/stack-utils@2.0.3': {} + + '@types/superagent@8.1.9': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 22.15.2 + form-data: 4.0.2 + + '@types/supertest@6.0.3': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.9 + + '@types/triple-beam@1.3.5': {} + + '@types/validator@13.15.0': {} + + '@types/winston@2.4.4': + dependencies: + winston: 3.17.0 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@8.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.31.0(eslint@9.25.1)(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.31.0 + '@typescript-eslint/type-utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3) + '@typescript-eslint/utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.31.0 + eslint: 9.25.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.31.0 + '@typescript-eslint/types': 8.31.0 + '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.31.0 + debug: 4.4.0(supports-color@5.5.0) + eslint: 9.25.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.31.0': + dependencies: + '@typescript-eslint/types': 8.31.0 + '@typescript-eslint/visitor-keys': 8.31.0 + + '@typescript-eslint/type-utils@8.31.0(eslint@9.25.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3) + debug: 4.4.0(supports-color@5.5.0) + eslint: 9.25.1 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.31.0': {} + + '@typescript-eslint/typescript-estree@8.31.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 8.31.0 + '@typescript-eslint/visitor-keys': 8.31.0 + debug: 4.4.0(supports-color@5.5.0) + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.1 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.31.0(eslint@9.25.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1) + '@typescript-eslint/scope-manager': 8.31.0 + '@typescript-eslint/types': 8.31.0 + '@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3) + eslint: 9.25.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.31.0': + dependencies: + '@typescript-eslint/types': 8.31.0 + eslint-visitor-keys: 4.2.0 + + '@vitejs/plugin-react@4.4.1(vite@6.3.3(@types/node@22.15.2))': + dependencies: + '@babel/core': 7.26.10 + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10) + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.3.3(@types/node@22.15.2) + transitivePeerDependencies: + - supports-color + + abbrev@1.1.1: {} + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-jsx@5.3.2(acorn@8.14.1): + dependencies: + acorn: 8.14.1 + + acorn@8.14.1: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + aproba@2.0.0: {} + + are-we-there-yet@2.0.0: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-flatten@1.1.1: {} + + array-includes@3.1.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + + array-union@2.1.0: {} + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + asap@2.0.6: {} + + async-function@1.0.0: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + aws-ssl-profiles@1.1.2: {} + + babel-jest@29.7.0(@babel/core@7.26.10): + dependencies: + '@babel/core': 7.26.10 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.26.10) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.26.5 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.7 + + babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.10): + dependencies: + '@babel/core': 7.26.10 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.10) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.10) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.10) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.26.10) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.10) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.10) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.26.10) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.10) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.10) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.10) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.10) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.26.10) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.10) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.10) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.10) + + babel-preset-jest@29.6.3(@babel/core@7.26.10): + dependencies: + '@babel/core': 7.26.10 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.10) + + balanced-match@1.0.2: {} + + bcrypt@5.1.1: + dependencies: + '@mapbox/node-pre-gyp': 1.0.11 + node-addon-api: 5.1.0 + transitivePeerDependencies: + - encoding + - supports-color + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + binary-extensions@2.3.0: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.0(supports-color@5.5.0) + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.24.4: + dependencies: + caniuse-lite: 1.0.30001715 + electron-to-chromium: 1.5.142 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.24.4) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001715: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + chardet@0.7.0: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@2.0.0: {} + + ci-info@3.9.0: {} + + cjs-module-lexer@1.4.3: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cls-rtracer@2.6.3: + dependencies: + uuid: 9.0.1 + + co@4.6.0: {} + + collect-v8-coverage@1.0.2: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color-support@1.1.3: {} + + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + + colorspace@1.1.4: + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@9.5.0: {} + + component-emitter@1.3.1: {} + + concat-map@0.0.1: {} + + console-control-strings@1.1.0: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + + cookiejar@2.1.4: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + create-jest@29.7.0(@types/node@22.15.2): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.15.2) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.1.3: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.0(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + dedent@1.5.3: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delayed-stream@1.0.0: {} + + delegates@1.0.0: {} + + denque@2.1.0: {} + + depd@2.0.0: {} + + destroy@1.2.0: {} + + detect-indent@6.1.0: {} + + detect-libc@2.0.4: {} + + detect-newline@3.1.0: {} + + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + + diff-sequences@29.6.3: {} + + dinero.js@1.9.1: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dotenv@16.0.3: {} + + dotenv@16.5.0: {} + + dottie@2.0.6: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.2 + + electron-to-chromium@1.5.142: {} + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + enabled@2.0.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.23.9: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild-register@3.6.0(esbuild@0.25.3): + dependencies: + debug: 4.4.0(supports-color@5.5.0) + esbuild: 0.25.3 + transitivePeerDependencies: + - supports-color + + esbuild@0.25.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.3 + '@esbuild/android-arm': 0.25.3 + '@esbuild/android-arm64': 0.25.3 + '@esbuild/android-x64': 0.25.3 + '@esbuild/darwin-arm64': 0.25.3 + '@esbuild/darwin-x64': 0.25.3 + '@esbuild/freebsd-arm64': 0.25.3 + '@esbuild/freebsd-x64': 0.25.3 + '@esbuild/linux-arm': 0.25.3 + '@esbuild/linux-arm64': 0.25.3 + '@esbuild/linux-ia32': 0.25.3 + '@esbuild/linux-loong64': 0.25.3 + '@esbuild/linux-mips64el': 0.25.3 + '@esbuild/linux-ppc64': 0.25.3 + '@esbuild/linux-riscv64': 0.25.3 + '@esbuild/linux-s390x': 0.25.3 + '@esbuild/linux-x64': 0.25.3 + '@esbuild/netbsd-arm64': 0.25.3 + '@esbuild/netbsd-x64': 0.25.3 + '@esbuild/openbsd-arm64': 0.25.3 + '@esbuild/openbsd-x64': 0.25.3 + '@esbuild/sunos-x64': 0.25.3 + '@esbuild/win32-arm64': 0.25.3 + '@esbuild/win32-ia32': 0.25.3 + '@esbuild/win32-x64': 0.25.3 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.2(eslint@9.25.1): + dependencies: + eslint: 9.25.1 + + eslint-config-turbo@2.5.2(eslint@9.25.1)(turbo@2.5.2): + dependencies: + eslint: 9.25.1 + eslint-plugin-turbo: 2.5.2(eslint@9.25.1)(turbo@2.5.2) + turbo: 2.5.2 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.25.1): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.31.0(eslint@9.25.1)(typescript@5.8.3) + eslint: 9.25.1 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.25.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.25.1) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.31.0(eslint@9.25.1)(typescript@5.8.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-only-warn@1.1.0: {} + + eslint-plugin-turbo@2.5.2(eslint@9.25.1)(turbo@2.5.2): + dependencies: + dotenv: 16.0.3 + eslint: 9.25.1 + turbo: 2.5.2 + + eslint-scope@8.3.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.25.1: + dependencies: + '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.20.0 + '@eslint/config-helpers': 0.2.1 + '@eslint/core': 0.13.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.25.1 + '@eslint/plugin-kit': 0.2.8 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.2 + '@types/estree': 1.0.7 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.0(supports-color@5.5.0) + escape-string-regexp: 4.0.0 + eslint-scope: 8.3.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) + eslint-visitor-keys: 4.2.0 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit@0.1.2: {} + + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + 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.13.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 + transitivePeerDependencies: + - supports-color + + extendable-error@0.1.7: {} + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-safe-stringify@2.1.1: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fdir@6.4.4(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fecha@4.2.3: {} + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-stream-rotator@0.6.1: + dependencies: + moment: 2.30.1 + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.1: + 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.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + fn.name@1.1.0: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.2.2 + dezalgo: 1.0.4 + once: 1.4.0 + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + gauge@3.0.2: + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + 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 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.10.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@11.12.0: {} + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-bigints@1.1.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-unicode@2.0.1: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + helmet@8.1.0: {} + + html-escaper@2.0.2: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-status@2.1.0: {} + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + human-id@4.1.1: {} + + human-signals@2.1.0: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore-by-default@1.0.1: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + inflection@1.13.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.3: {} + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + ipaddr.js@1.9.1: {} + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.2: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-property@1.0.2: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@2.0.1: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-windows@1.0.2: {} + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.26.10 + '@babel/parser': 7.27.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.26.10 + '@babel/parser': 7.27.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.2 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.3 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@22.15.2): + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.15.2) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.15.2) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@22.15.2): + dependencies: + '@babel/core': 7.26.10 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.10) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.15.2 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.2 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 22.15.2 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.26.2 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.15.2 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.10 + resolve.exports: 2.0.3 + slash: 3.0.0 + + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.2 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.2 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.26.10 + '@babel/generator': 7.27.0 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.10) + '@babel/types': 7.27.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.10) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.7.1 + transitivePeerDependencies: + - supports-color + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.15.2 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.15.2 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@29.7.0: + dependencies: + '@types/node': 22.15.2 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@22.15.2): + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.15.2) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.1 + + jwa@1.4.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + kuler@2.0.0: {} + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + libphonenumber-js@1.12.7: {} + + lines-and-columns@1.2.4: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + 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.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: {} + + lodash.startcase@4.4.0: {} + + lodash@4.17.21: {} + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + long@5.3.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@7.18.3: {} + + lru.min@1.1.2: {} + + luxon@3.6.1: {} + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.1 + + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@1.0.3: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + mime@1.6.0: {} + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp@1.0.4: {} + + module-alias@2.2.3: {} + + moment-timezone@0.5.48: + dependencies: + moment: 2.30.1 + + moment@2.30.1: {} + + mri@1.2.0: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + mylas@2.1.13: {} + + mysql2@3.14.0: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.6.3 + long: 5.3.2 + lru.min: 1.1.2 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.3: + dependencies: + lru-cache: 7.18.3 + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + negotiator@0.6.3: {} + + node-addon-api@5.1.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-int64@0.4.0: {} + + node-releases@2.0.19: {} + + nodemon@3.1.10: + dependencies: + chokidar: 3.6.0 + debug: 4.4.0(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.7.1 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npmlog@5.0.1: + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.0.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + os-tmpdir@1.0.2: {} + + outdent@0.5.0: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.10 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parseurl@1.3.3: {} + + passport-jwt@4.0.1: + dependencies: + jsonwebtoken: 9.0.2 + passport-strategy: 1.0.0 + + passport-local@1.0.0: + dependencies: + passport-strategy: 1.0.0 + + passport-strategy@1.0.0: {} + + passport@0.7.0: + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-to-regexp@0.1.12: {} + + path-type@4.0.0: {} + + path@0.12.7: + dependencies: + process: 0.11.10 + util: 0.10.4 + + pause@0.0.1: {} + + pg-connection-string@2.8.5: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pify@4.0.1: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + plimit-lit@1.6.1: + dependencies: + queue-lit: 1.5.2 + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.3: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@2.8.8: {} + + prettier@3.5.3: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + process@0.11.10: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pstree.remy@1.1.8: {} + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + quansync@0.2.10: {} + + queue-lit@1.5.2: {} + + queue-microtask@1.2.3: {} + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + + react-is@18.3.1: {} + + react-refresh@0.17.0: {} + + react@19.1.0: {} + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + reflect-metadata@0.2.2: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regenerator-runtime@0.14.1: {} + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-directory@2.1.1: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve.exports@2.0.3: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + response-time@2.3.3: + dependencies: + depd: 2.0.0 + on-headers: 1.0.2 + + retry-as-promised@7.1.1: {} + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.40.0: + dependencies: + '@types/estree': 1.0.7 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.40.0 + '@rollup/rollup-android-arm64': 4.40.0 + '@rollup/rollup-darwin-arm64': 4.40.0 + '@rollup/rollup-darwin-x64': 4.40.0 + '@rollup/rollup-freebsd-arm64': 4.40.0 + '@rollup/rollup-freebsd-x64': 4.40.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.40.0 + '@rollup/rollup-linux-arm-musleabihf': 4.40.0 + '@rollup/rollup-linux-arm64-gnu': 4.40.0 + '@rollup/rollup-linux-arm64-musl': 4.40.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.40.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0 + '@rollup/rollup-linux-riscv64-gnu': 4.40.0 + '@rollup/rollup-linux-riscv64-musl': 4.40.0 + '@rollup/rollup-linux-s390x-gnu': 4.40.0 + '@rollup/rollup-linux-x64-gnu': 4.40.0 + '@rollup/rollup-linux-x64-musl': 4.40.0 + '@rollup/rollup-win32-arm64-msvc': 4.40.0 + '@rollup/rollup-win32-ia32-msvc': 4.40.0 + '@rollup/rollup-win32-x64-msvc': 4.40.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + scheduler@0.26.0: {} + + semver@6.3.1: {} + + semver@7.7.1: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + seq-queue@0.0.5: {} + + sequelize-pool@7.1.0: {} + + sequelize@6.37.7(mysql2@3.14.0): + dependencies: + '@types/debug': 4.1.12 + '@types/validator': 13.15.0 + debug: 4.4.0(supports-color@5.5.0) + dottie: 2.0.6 + inflection: 1.13.4 + lodash: 4.17.21 + moment: 2.30.1 + moment-timezone: 0.5.48 + pg-connection-string: 2.8.5 + retry-as-promised: 7.1.1 + semver: 7.7.1 + sequelize-pool: 7.1.0 + toposort-class: 1.0.1 + uuid: 8.3.2 + validator: 13.15.0 + wkx: 0.5.0 + optionalDependencies: + mysql2: 3.14.0 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + setprototypeof@1.2.0: {} + + shallow-equal-object@1.1.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.1 + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + sprintf-js@1.0.3: {} + + sqlstring@2.3.3: {} + + stack-trace@0.0.10: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + statuses@2.0.1: {} + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + superagent@9.0.2: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.0(supports-color@5.5.0) + fast-safe-stringify: 2.1.1 + form-data: 4.0.2 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.14.0 + transitivePeerDependencies: + - supports-color + + supertest@7.1.0: + dependencies: + methods: 1.1.2 + superagent: 9.0.2 + transitivePeerDependencies: + - supports-color + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + term-size@2.2.1: {} + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-hex@1.0.0: {} + + tinyglobby@0.2.13: + dependencies: + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + toposort-class@1.0.1: {} + + touch@3.1.1: {} + + tr46@0.0.3: {} + + triple-beam@1.4.1: {} + + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + ts-jest@29.3.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest@29.7.0)(typescript@5.8.3): + dependencies: + bs-logger: 0.2.6 + ejs: 3.1.10 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@22.15.2) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.1 + type-fest: 4.40.0 + typescript: 5.8.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.26.10 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.10) + + tsc-alias@1.8.15: + dependencies: + chokidar: 3.6.0 + commander: 9.5.0 + get-tsconfig: 4.10.0 + globby: 11.1.0 + mylas: 2.1.13 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + turbo-darwin-64@2.5.2: + optional: true + + turbo-darwin-arm64@2.5.2: + optional: true + + turbo-linux-64@2.5.2: + optional: true + + turbo-linux-arm64@2.5.2: + optional: true + + turbo-windows-64@2.5.2: + optional: true + + turbo-windows-arm64@2.5.2: + optional: true + + turbo@2.5.2: + optionalDependencies: + turbo-darwin-64: 2.5.2 + turbo-darwin-arm64: 2.5.2 + turbo-linux-64: 2.5.2 + turbo-linux-arm64: 2.5.2 + turbo-windows-64: 2.5.2 + turbo-windows-arm64: 2.5.2 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + type-fest@4.40.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.8.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undefsafe@2.0.5: {} + + undici-types@6.21.0: {} + + universalify@0.1.2: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.1.3(browserslist@4.24.4): + dependencies: + browserslist: 4.24.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + util@0.10.4: + dependencies: + inherits: 2.0.3 + + utils-merge@1.0.1: {} + + uuid@11.1.0: {} + + uuid@8.3.2: {} + + uuid@9.0.1: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + validator@13.15.0: {} + + vary@1.1.2: {} + + vite@6.3.3(@types/node@22.15.2): + dependencies: + esbuild: 0.25.3 + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.3 + rollup: 4.40.0 + tinyglobby: 0.2.13 + optionalDependencies: + '@types/node': 22.15.2 + fsevents: 2.3.3 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + 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 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + + winston-daily-rotate-file@5.0.0(winston@3.17.0): + dependencies: + file-stream-rotator: 0.6.1 + object-hash: 3.0.0 + triple-beam: 1.4.1 + winston: 3.17.0 + winston-transport: 4.9.0 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.17.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + + wkx@0.5.0: + dependencies: + '@types/node': 22.15.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zod@3.24.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..6e0abd9 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +packages: + - apps/* + - modules/* + - packages/* + - "!packages/rdx-auth/*" diff --git a/scripts/create-package.ts b/scripts/create-package.ts new file mode 100644 index 0000000..cea94d7 --- /dev/null +++ b/scripts/create-package.ts @@ -0,0 +1,65 @@ +import fs from "fs"; +import inquirer from "inquirer"; +import path from "path"; + +function capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +async function main() { + let rawName = process.argv[2]; + + if (!rawName) { + const answers = await inquirer.prompt([ + { + type: "input", + name: "packageName", + message: "Nombre del nuevo package:", + validate: (input) => (input ? true : "El nombre no puede estar vacío"), + }, + ]); + rawName = answers.packageName; + } + + const name = rawName.toLowerCase(); + const capitalized = capitalize(name); + const packagePath = path.resolve(__dirname, "../packages", name); + const clientPath = path.join(packagePath, "client"); + const serverPath = path.join(packagePath, "server"); + const templatePath = path.resolve(__dirname, "./templates"); + + // Crear carpetas + fs.mkdirSync(clientPath, { recursive: true }); + fs.mkdirSync(serverPath, { recursive: true }); + + // Función de reemplazo de contenido + const renderTemplate = (content: string) => + content + .replace(/__PACKAGE_NAME__/g, name) + .replace(/__PACKAGE_NAME_CAPITALIZED__/g, capitalized); + + // Copiar plantillas desde carpeta 'templates/client' + const copyFromTemplate = (srcDir: string, destDir: string) => { + const files = fs.readdirSync(srcDir); + for (const file of files) { + const filePath = path.join(srcDir, file); + const content = fs.readFileSync(filePath, "utf-8"); + + const outputName = file.replace(/__PACKAGE_NAME__/g, capitalized); + const outputPath = path.join(destDir, outputName); + + fs.writeFileSync(outputPath, renderTemplate(content)); + } + }; + + copyFromTemplate(path.join(templatePath, "client"), clientPath); + copyFromTemplate(path.join(templatePath, "server"), serverPath); + + // package.json + const pkgJsonTemplate = fs.readFileSync(path.join(templatePath, "package.json"), "utf-8"); + fs.writeFileSync(path.join(packagePath, "package.json"), renderTemplate(pkgJsonTemplate)); + + console.log(`✅ Package '${name}' creado correctamente en packages/${name}`); +} + +main(); diff --git a/scripts/templates/client/__PACKAGE_NAME__Page.tsx b/scripts/templates/client/__PACKAGE_NAME__Page.tsx new file mode 100644 index 0000000..443aef5 --- /dev/null +++ b/scripts/templates/client/__PACKAGE_NAME__Page.tsx @@ -0,0 +1,3 @@ +export default function __PACKAGE_NAME_CAPITALIZED__Page() { + return
__PACKAGE_NAME_CAPITALIZED__ Package Page
; +} diff --git a/scripts/templates/client/manifest.ts b/scripts/templates/client/manifest.ts new file mode 100644 index 0000000..165d980 --- /dev/null +++ b/scripts/templates/client/manifest.ts @@ -0,0 +1,12 @@ +import { IPackageClient } from "@packages/package"; +import __PACKAGE_NAME_CAPITALIZED__Page from "./__PACKAGE_NAME__Page"; + +export const __PACKAGE_NAME_CAPITALIZED__Package: IPackageClient = { + metadata: { + name: "__PACKAGE_NAME__", + route: "/__PACKAGE_NAME__", + version: "1.0.0", + description: "__PACKAGE_NAME_CAPITALIZED__ package", + }, + component: __PACKAGE_NAME_CAPITALIZED__Page, +}; diff --git a/scripts/templates/client/tsconfig.json b/scripts/templates/client/tsconfig.json new file mode 100644 index 0000000..bbb38ed --- /dev/null +++ b/scripts/templates/client/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@repo/typescript-config/react", + "include": ["."] +} diff --git a/scripts/templates/package.json b/scripts/templates/package.json new file mode 100644 index 0000000..8295f99 --- /dev/null +++ b/scripts/templates/package.json @@ -0,0 +1,23 @@ +{ + "name": "@packages/__PACKAGE_NAME__", + "version": "1.0.0", + "private": true, + "main": "server/index.ts", + "scripts": { + "build": "tsc -b", + "dev": "turbo run dev --filter=@packages/__PACKAGE_NAME__" + }, + "dependencies": {}, + "devDependencies": {}, + "peerDependencies": { + "@types/express": "*", + "@types/node": "*", + "@types/react": "*", + "express": "*", + "react": "*", + "react-router-dom": "*", + "sequelize": "*", + "typescript": "*", + "zod": "*" + } +} diff --git a/scripts/templates/server/controller.ts b/scripts/templates/server/controller.ts new file mode 100644 index 0000000..7e0c752 --- /dev/null +++ b/scripts/templates/server/controller.ts @@ -0,0 +1,3 @@ +export function __PACKAGE_NAME__Controller(req, res) { + res.send("__PACKAGE_NAME_CAPITALIZED__ package response"); +} diff --git a/scripts/templates/server/index.ts b/scripts/templates/server/index.ts new file mode 100644 index 0000000..26750cf --- /dev/null +++ b/scripts/templates/server/index.ts @@ -0,0 +1,13 @@ +import { IPackageServer } from "@packages/package"; +import { __PACKAGE_NAME__Controller } from "./controller"; + +export const __PACKAGE_NAME_CAPITALIZED__Package: IPackageServer = { + metadata: { + name: "__PACKAGE_NAME__", + version: "1.0.0", + dependencies: [], + }, + init(app) { + app.get("/__PACKAGE_NAME__", __PACKAGE_NAME__Controller); + }, +}; diff --git a/scripts/templates/server/tsconfig.json b/scripts/templates/server/tsconfig.json new file mode 100644 index 0000000..0593b22 --- /dev/null +++ b/scripts/templates/server/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@repo/typescript-config/node", + "include": ["."], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + } +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..38864b9 --- /dev/null +++ b/turbo.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://turborepo.com/schema.json", + "ui": "tui", + "tasks": { + "build": { + "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + }, + "test": { + "outputs": ["coverage/**"], + "dependsOn": [] + }, + "lint": { + "dependsOn": ["^build"] + }, + "dev": { + "dependsOn": ["^build"], + "cache": false, + "persistent": true + }, + "clean": { + "cache": false + } + } +}